# Tutorial: Integrating Chainlink Oracles with Streams

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 18+ and npm.
* `@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
```

#### 1. 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
```

#### 2. 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: somniaTestnet,
  transport: http(getEnv('RPC_URL_SOMNIA')),
})

export const somniaSdk = new SDK({
  public: somniaPublicClient,
  wallet: somniaWalletClient,
})

// === Client 2: Sepolia Public Client (Read-Only) ===
export const sepoliaPublicClient = createPublicClient({
  chain: sepolia,
  transport: http(getEnv('RPC_URL_SEPOLIA')),
})
```

### Step 1: Define the Price Feed Schema

Our schema will store the core data from Chainlink's feed.

**`src/lib/schema.ts`**

```typescript
// This schema will store historical price snapshots
export const priceFeedSchema = 
  'uint64 timestamp, int256 price, uint80 roundId, string pair'
```

* `timestamp`: The `updatedAt` time from Chainlink.
* `price`: The `answer` (e.g., ETH price).
* `roundId`: The Chainlink round ID, to prevent duplicates.
* `pair`: A string to identify the feed (e.g., "ETH/USD").

### Step 2: Create the Chainlink Reader

Let's create a dedicated file to handle fetching data from Chainlink. We will use the `ETH/USD` feed on Sepolia.

**`src/lib/chainlinkReader.ts`**

```typescript
import { parseAbi, Address } from 'viem'
import { sepoliaPublicClient } from './clients'

// Chainlink ETH/USD Feed on Sepolia Testnet
const CHAINLINK_FEED_ADDRESS: Address = '0x694AA1769357215DE4FAC081bf1f309aDC325306'

// Minimal ABI for AggregatorV3Interface
const CHAINLINK_ABI = parseAbi([
  'function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)',
  'function decimals() external view returns (uint8)',
])

export interface PriceData {
  roundId: bigint
  price: bigint
  timestamp: bigint
  decimals: number
}

/**
 * Fetches the latest price data from the Chainlink ETH/USD feed on Sepolia.
 */
export async function fetchLatestPrice(): Promise<PriceData> {
  console.log('Fetching latest price from Chainlink on Sepolia...')
  
  try {
    const [roundData, decimals] = await Promise.all([
      sepoliaPublicClient.readContract({
        address: CHAINLINK_FEED_ADDRESS,
        abi: CHAINLINK_ABI,
        functionName: 'latestRoundData',
      }),
      sepoliaPublicClient.readContract({
        address: CHAINLINK_FEED_ADDRESS,
        abi: CHAINLINK_ABI,
        functionName: 'decimals',
      })
    ])

    const [roundId, answer, , updatedAt] = roundData
    
    console.log(`Chainlink data received: Round ${roundId}, Price ${answer}`)
    
    return {
      roundId,
      price: answer,
      timestamp: updatedAt,
      decimals,
    }
  } catch (error: any) {
    console.error(`Failed to read from Chainlink: ${error.message}`)
    throw error
  }
}
```

### Step 3: Build the Snapshot Bot (The Hybrid App)

This is the core of our project. This script will:

1. Fetch the latest price from Chainlink (using our module).
2. Encode this data using our `priceFeedSchema`.
3. Publish the data to Somnia Data Streams.

**`src/scripts/snapshotBot.ts`**

```typescript
import 'dotenv/config'
import { somniaSdk } from '../lib/clients'
import { priceFeedSchema } from '../lib/schema'
import { fetchLatestPrice } from '../lib/chainlinkReader'
import { SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { toHex, Hex } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'

const PAIR_NAME = "ETH/USD"

async function main() {
  console.log('--- Starting Snapshot Bot ---')
  
  // 1. Initialize SDK and Encoder
  const sdk = somniaSdk
  const encoder = new SchemaEncoder(priceFeedSchema)
  const publisherAddress = sdk.wallet.account?.address
  if (!publisherAddress) throw new Error('Wallet client not initialized.')

  // 2. Compute Schema ID
  const schemaId = await sdk.streams.computeSchemaId(priceFeedSchema)
  if (!schemaId) throw new Error('Could not compute schemaId')
  
  console.log('Registering priceFeedSchema (if not already registered)...')
  
  // UPDATED: Using schemaName and ignoreRegisteredSchemas flag
  const ignoreAlreadyRegisteredSchemas = true
  const regTx = await sdk.streams.registerDataSchemas([
    { 
      schemaName: 'price-feed-v1', 
      schema: priceFeedSchema, 
      parentSchemaId: zeroBytes32 
    }
  ], ignoreAlreadyRegisteredSchemas)

  // regTx will be null if the schema was already registered.
  if (regTx) {
    console.log('Schema registration transaction sent:', regTx)
    await waitForTransactionReceipt(sdk.public, { hash: regTx })
    console.log('Schema registered successfully!')
  } else {
    console.log('Schema was already registered. No transaction sent.')
  }

  // 3. Fetch data from Chainlink
  const priceData = await fetchLatestPrice()

  // 4. Encode data for Somnia Streams
  const encodedData: Hex = encoder.encodeData([
    { name: 'timestamp', value: priceData.timestamp.toString(), type: 'uint64' },
    { name: 'price', value: priceData.price.toString(), type: 'int256' },
    { name: 'roundId', value: priceData.roundId.toString(), type: 'uint80' },
    { name: 'pair', value: PAIR_NAME, type: 'string' },
  ])

  // 5. Create a unique Data ID (using the roundId to prevent duplicates)
  const dataId = toHex(`price-${PAIR_NAME}-${priceData.roundId}`, { size: 32 })

  // 6. Publish to Somnia Data Streams
  console.log(`Publishing price data to Somnia Streams...`)
  const txHash = await sdk.streams.set([
    { id: dataId, schemaId, data: encodedData }
  ])

  if (!txHash) throw new Error('Failed to publish to Streams')
  
  await waitForTransactionReceipt(sdk.public, { hash: txHash })
  
  console.log('\n--- Snapshot Complete! ---')
  console.log(`  Publisher: ${publisherAddress}`)
  console.log(`  Schema ID: ${schemaId}`)
  console.log(`  Data ID: ${dataId}`)
  console.log(`  Tx Hash: ${txHash}`)
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

**To run your bot:**

Add a script to `package.json`: `"snapshot": "ts-node src/scripts/snapshotBot.ts"`

Run it: `npm run snapshot`

You can run this script multiple times. It will only add new data if Chainlink's `roundId` has changed.

### Step 4: Read Your Historical Price Feed

Now for the payoff. Let's create a script that reads our new on-chain history from Somnia Streams.

**`src/scripts/readHistory.ts`**

```typescript
import 'dotenv/config'
import { somniaSdk } from '../lib/clients'
import { priceFeedSchema } from '../lib/schema'
import { SchemaDecodedItem } from '@somnia-chain/streams'

// Helper to decode the SDK's output
interface PriceRecord {
  timestamp: number
  price: bigint
  roundId: bigint
  pair: string
}

function decodePriceRecord(row: SchemaDecodedItem[]): PriceRecord {
  const val = (field: any) => field?.value?.value ?? field?.value ?? ''
  return {
    timestamp: Number(val(row[0])),
    price: BigInt(val(row[1])),
    roundId: BigInt(val(row[2])),
    pair: String(val(row[3])),
  }
}

async function main() {
  console.log('--- Reading Historical Price Feed from Somnia Streams ---')
  const sdk = somniaSdk
  
  // Use the *publisher address* from your .env file
  const publisherAddress = sdk.wallet.account?.address
  if (!publisherAddress) throw new Error('Wallet client not initialized.')

  const schemaId = await sdk.streams.computeSchemaId(priceFeedSchema)
  if (!schemaId) throw new Error('Could not compute schemaId')

  console.log(`Reading all data for publisher: ${publisherAddress}`)
  console.log(`Schema: ${schemaId}\n`)

  // Fetch all data for this schema and publisher
  const data = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)

  if (!data || data.length === 0) {
    console.log('No price history found. Run the snapshot bot first.')
    return
  }

  const records = (data as SchemaDecodedItem[][]).map(decodePriceRecord)
  
  // Sort by timestamp
  records.sort((a, b) => a.timestamp - b.timestamp)

  console.log(`Found ${records.length} historical price points:\n`)
  
  records.forEach(record => {
    // We assume the decimals are 8 for this ETH/USD feed
    const priceFloat = Number(record.price) / 10**8
    console.log(
      `[${new Date(record.timestamp * 1000).toISOString()}] ${record.pair} - $${priceFloat.toFixed(2)} (Round: ${record.roundId})`
    )
  })
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

**To read the history:**

Add to `package.json`: `"history": "ts-node src/scripts/readHistory.ts"`

Run it: `npm run history`

**Expected Output:**

```bash
--- Reading Historical Price Feed from Somnia Streams ---
...
Found 3 historical price points:

[2025-11-06T14:30:00.000Z] ETH/USD - $3344.50 (Round: 110...)
[2025-11-06T14:35:00.000Z] ETH/USD - $3362.12 (Round: 111...)
[2025-11-06T14:40:00.000Z] ETH/USD - $3343.90 (Round: 112...)
```

### Conclusion: Key Takeaways

You have successfully built a hybrid, cross-chain application.

* You combined an **external "truth source"** (Chainlink) with Somnia's **composable storage layer** (Somnia Data Streams).
* You created a new, valuable, on-chain data product: a **historical, queryable price feed** that any dApp on Somnia can now read from and trust.
* You demonstrated the power of the `publisher` address as a verifiable source. Any dApp can now consume your feed, knowing it was published by *your* trusted bot.

This pattern can be extended to any external data source: weather, sports results, IoT data, and more. You can run the `snapshotBot.ts` script as a cron job or serverless function to create a truly autonomous, on-chain oracle.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://emre-gitbook.gitbook.io/emre-gitbook-docs/data-streams/tutorial-integrating-chainlink-oracles-with-streams.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
