Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
module.exports = {
// ...
networks: {
somnia: {
url: "https://api.infra.mainnet.somnia.network",
accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
},
},
// ...
};forge create --rpc-url https://api.infra.mainnet.somnia.network --private-key PRIVATE_KEY src/Example.sol:ExampleHands-on guides to build from scratch
web3.eth.subscribe('logs', { address: '0x...' }, (err, log) => {
// Now pull state manually
contract.methods.balanceOf(...).call();
});sdk.subscribe({
ethCalls: [{
to: contractAddress,
data: encodeFunctionData({ abi, functionName: 'balanceOf', args: [userAddress] })
}],
onData: (data) => {
// Event + state (including balanceOf result) delivered atomically with `data`
}
});





import { SDK } from "@somnia-chain/streams"
import { zeroAddress, erc721Abi } from "viem"
// Use WebSocket transport in the public client for subscription tasks
// For the SDK instance that executes transactions, stick with htttp
const sdk = new SDK({
public: getPublicClient(),
wallet: getWalletClient(),
})
// Encode view function calls to be executed when an event takes place
const ethCalls = [{
to: "0x23B66B772AE29708a884cca2f9dec0e0c278bA2c",
data: encodeFunctionData({
abi: erc721Abi,
functionName: "balanceOf",
args: ["0x3dC360e0389683cA0341a11Fc3bC26252b5AF9bA"]
})
}]
// Start a subsciption
const subscription = await sdk.streams.subscribe({
ethCalls,
onData: (data) => {
const decodedLog = decodeEventLog({
abi: fireworkABI,
topics: data.result.topics,
data: data.result.data,
});
const decodedFunctionResult = decodeFunctionResult({
abi: erc721Abi,
functionName: 'balanceOf',
data: data.result.simulationResults[0],
});
console.log("Decoded event", decodedLog);
console.log("Decoded function call result", decodedFunctionResult);
}
})
// Write data and emit events that will trigger the above callback!
const dataStreams = [{
id,
schemaId: driverSchemaId,
data: encodedData
}]
const eventStreams = [{
id: somniaStreamsEventId,
argumentTopics,
data
}]
const setAndEmitEventsTxHash = await sdk.streams.setAndEmitEvents(
dataStreams,
eventStreams
)interface ISomniaReactivityPrecompile {
struct SubscriptionData {
bytes32[4] eventTopics; // Topic filter (0x0 for wildcard)
address origin; // Origin (tx.origin) filter (address(0) for wildcard)
address caller; // Reserved for future use (address(0) for wildcard)
address emitter; // Contract emitting the event (address(0) for wildcard)
address handlerContractAddress; // Address of the contract to handle the event
bytes4 handlerFunctionSelector; // Function selector in the handler contract
uint64 priorityFeePerGas; // Extra fee to prioritize handling, in nanoSOMI
uint64 maxFeePerGas; // Max fee willing to pay, in nanoSOMI
uint64 gasLimit; // Maximum gas that will be provisioned per subscription callback
bool isGuaranteed; // If true, moves to next block if current is full
bool isCoalesced; // If true, multiple events can be coalesced
}
// System events
event BlockTick(uint64 indexed blockNumber);
event EpochTick(uint64 indexed epochNumber, uint64 indexed blockNumber);
event Schedule(uint256 indexed timestampMillis);
event SubscriptionCreated(uint64 indexed subscriptionId, address indexed owner);
event SubscriptionRemoved(uint64 indexed subscriptionId, address indexed owner);
function subscribe(SubscriptionData calldata subscriptionData) external returns (uint256 subscriptionId);
function unsubscribe(uint256 subscriptionId) external;
function getSubscriptionInfo(uint256 subscriptionId) external view returns (SubscriptionData memory subscriptionData, address owner);
}npx create-next-app@latest somnia-connectkit
cd somnia-connectkitnpm install wagmi viem @tanstack/react-query connectkit'use client';
import { WagmiConfig, createConfig } from 'wagmi';
import { ConnectKitProvider, getDefaultConfig } from 'connectkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { somniaTestnet } from 'viem/chains';
const queryClient = new QueryClient();
const config = createConfig(
getDefaultConfig({
autoConnect: true,
appName: 'Somnia DApp',
chains: [somniaTestnet],
})
);
export default function ClientProvider({ children }) {
return (
<WagmiConfig config={config}>
<QueryClientProvider client={queryClient}>
<ConnectKitProvider>{children}</ConnectKitProvider>
</QueryClientProvider>
</WagmiConfig>
);
}import ClientProvider from './components/ClientProvider';
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<title>Somnia DApp</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<ClientProvider>{children}</ClientProvider>
</body>
</html>
);
}'use client';
import { useAccount } from 'wagmi';
import { ConnectKitButton } from 'connectkit';
export default function Home() {
const { address, isConnected } = useAccount();
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
Hello, world!
{/* Connect Button */}
<div className="mt-4">
<ConnectKitButton />
</div>
{/* Show Wallet Address */}
{isConnected && (
<p className="mt-4 text-lg text-blue-600">Connected as: {address}</p>
)}
</main>
</div>
);
}npm run devimport { SDK } from "@somnia-chain/streams"
const sdk = new SDK({
public: getPublicClient(),
wallet: getWalletClient(),
})
// The parent schema here will be the GPS schema from the quick start guide
const gpsSchema = `uint64 timestamp, int32 latitude, int32 longitude, int32 altitude, uint32 accuracy, bytes32 entityId, uint256 nonce`
const parentSchemaId = await sdk.streams.computeSchemaId(gpsSchema)
// Lets extend the gps schema and add F1 data since every car will have a gps position
const formulaOneSchema = `uint256 driverNumber`
// We can also extend the gps schema for FR data i.e. aircraft identifier
const flightRadarSchema = `bytes32 ICAO24`
await sdk.streams.registerDataSchemas([
{ schemaName: "gps", schema: gpsSchema },
{ schemaName: "f1", schema: formulaOneSchema, parentSchemaId }, // F1 extends GPS
{ schemaName: "FR", schema: flightRadarSchema, parentSchemaId },// FR extends GPS
])import { SDK } from "@somnia-chain/streams"
const sdk = new SDK({
public: getPublicClient(),
wallet: getWalletClient(),
})
const versionSchema = `uint16 version`
const parentSchemaId = await sdk.streams.computeSchemaId(versionSchema)
// Now lets register a person schema with expectation there will be many versions of the person schema
const personSchema = `uint8 age`
await sdk.streams.registerDataSchemas([
{ schemaName: "version", schema: versionSchema },
{ schemaName: "person", schema: personSchema, parentSchemaId }
])


Get started with reactivity tools. Split by environment for clarity.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract Greeter {
string public name;
address public owner;
event NameChanged(string oldName, string newName);
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can perform this action");
_;
}
constructor(string memory _initialName) {
name = _initialName;
owner = msg.sender;
}
function changeName(string memory _newName) external onlyOwner {
string memory oldName = name;
name = _newName;
emit NameChanged(oldName, _newName);
}
function greet() external view returns (string memory) {
return string(abi.encodePacked("Hello, ", name, "!"));
}
}forge init BallotVoting// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract BallotVoting {
struct Ballot {
string name;
string[] options;
mapping(uint256 => uint256) votes;
mapping(address => bool) hasVoted;
bool active;
uint256 totalVotes;
}
uint256 public ballotCount;
mapping(uint256 => Ballot) public ballots;
event BallotCreated(uint256 indexed ballotId, string name, string[] options);
event VoteCast(uint256 indexed ballotId, address indexed voter, uint256 optionIndex);
event BallotClosed(uint256 indexed ballotId);
function createBallot(string memory name, string[] memory options) public {
require(options.length > 1, "Ballot must have at least two options");
ballotCount++;
Ballot storage ballot = ballots[ballotCount];
ballot.name = name;
ballot.options = options;
ballot.active = true;
emit BallotCreated(ballotCount, name, options);
}
function vote(uint256 ballotId, uint256 optionIndex) public {
Ballot storage ballot = ballots[ballotId];
require(ballot.active, "This ballot is closed");
require(!ballot.hasVoted[msg.sender], "You have already voted");
require(optionIndex < ballot.options.length, "Invalid option index");
ballot.votes[optionIndex]++;
ballot.hasVoted[msg.sender] = true;
ballot.totalVotes++;
emit VoteCast(ballotId, msg.sender, optionIndex);
}
function closeBallot(uint256 ballotId) public {
Ballot storage ballot = ballots[ballotId];
require(ballot.active, "Ballot is already closed");
ballot.active = false;
emit BallotClosed(ballotId);
}
function getBallotDetails(uint256 ballotId)
public
view
returns (
string memory name,
string[] memory options,
bool active,
uint256 totalVotes
)
{
Ballot storage ballot = ballots[ballotId];
return (ballot.name, ballot.options, ballot.active, ballot.totalVotes);
}
function getBallotResults(uint256 ballotId) public view returns (uint256[] memory results) {
Ballot storage ballot = ballots[ballotId];
uint256[] memory voteCounts = new uint256[](ballot.options.length);
for (uint256 i = 0; i < ballot.options.length; i++) {
voteCounts[i] = ballot.votes[i];
}
return voteCounts;
}
}forge build[⠊] Compiling...
[⠢] Compiling 27 files with Solc 0.8.28
[⠆] Solc 0.8.28 finished in 2.22s
Compiler run successful!forge create --rpc-url
https://dream-rpc.somnia.network
--private-key PRIVATE_KEY src/BallotVoting.sol:BallotVoting[⠊] Compiling...
No files changed, compilation skipped
Deployer: 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03
Deployed to: 0x46639fB6Ce28FceC29993Fc0201Cd5B6fb1b7b16
Transaction hash: 0xb3f8fe0443acae4efdb6d642bbadbb66797ae1dcde2c864d5c00a56302fb9a34import { SDK } from '@somnia-chain/reactivity';
// Assuming you have an instance of SomniaReactivity SDK
const reactivity = new SDK(/* your config */);
async function setupBlockTick() {
try {
const txHash = await reactivity.createOnchainBlockTickSubscription({
// Optional: Specify a future block number; omit for every block
// blockNumber: BigInt(123456789),
handlerContractAddress: '0xYourHandlerContractAddress',
// Optional: Override default handler selector (defaults to onEvent)
// handlerFunctionSelector: '0xYourSelector',
priorityFeePerGas: BigInt(1000000000), // 1 nanoSOMI
maxFeePerGas: BigInt(20000000000), // 20 nanoSOMI
gasLimit: BigInt(2000000),
isGuaranteed: true, // Ensure delivery even if delayed
isCoalesced: false // Handle each event separately
});
console.log('Subscription created with tx hash:', txHash);
} catch (error) {
console.error('Error creating subscription:', error);
}
}
setupBlockTick();ISomniaReactivityPrecompile.SubscriptionData
memory subscriptionData = ISomniaReactivityPrecompile
.SubscriptionData({
eventTopics: [BlockTick.selector, bytes32(0), bytes32(0), bytes32(0)], // Or specify blockNumber in topics[1]
emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
handlerContractAddress: address(this),
handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
/* Add gas params, isGuaranteed, isCoalesced here */
});
// Then call subscribe(subscriptionData) on the precompileimport { SDK } from '@somnia-chain/reactivity';
// Assuming you have an instance of SomniaReactivity
const reactivity = new SDK(/* your config */);
async function setupSchedule() {
try {
const txHash = await reactivity.scheduleOnchainCronJob({
timestampMs: 1794395471011, // e.g., Nov 11, 2026, 11:11:11.011
handlerContractAddress: '0xYourHandlerContractAddress',
// Optional: Override default handler selector (defaults to onEvent)
// handlerFunctionSelector: '0xYourSelector',
priorityFeePerGas: BigInt(1000000000), // 1 nanoSOMI
maxFeePerGas: BigInt(20000000000), // 20 nanoSOMI
gasLimit: BigInt(2000000),
isGuaranteed: true, // Ensure delivery even if delayed
isCoalesced: false // N/A for one-off, but included for consistency
});
console.log('Cron job scheduled with tx hash:', txHash);
} catch (error) {
console.error('Error scheduling cron job:', error);
}
}
setupSchedule();ISomniaReactivityPrecompile.SubscriptionData
memory subscriptionData = ISomniaReactivityPrecompile
.SubscriptionData({
eventTopics: [Schedule.selector, bytes32(uint256(1794395471011)), bytes32(0), bytes32(0)],
emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
handlerContractAddress: address(this),
handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
/* Add gas params, isGuaranteed, isCoalesced here */
});
// Then call subscribe(subscriptionData) on the precompilenpx create-next-app metamask-example <p>Hello, World!</p>npm i viemimport { useState } from "react";
import {
createPublicClient,
http,
createWalletClient,
} from "viem";import { somniaTestnet } from "viem/chains";const [address, setAddress] = useState<string>("");
const [connected, setConnected] = useState(false);const connectToMetaMask = async () => {
if (typeof window !== "undefined" && window.ethereum !== undefined) {
try {
await window.ethereum.request({ method: "eth_requestAccounts" });
const walletClient = createWalletClient({
chain: SOMNIA,
transport: custom(window.ethereum),
});
const [userAddress] = await walletClient.getAddresses();
setClient(walletClient);
setAddress(userAddress);
setConnected(true);
console.log("Connected account:", userAddress);
} catch (error) {
console.error("User denied account access:", error);
}
} else {
console.log(
"MetaMask is not installed or not running in a browser environment!"
);
}
};{!connected ? (
<button
onClick={connectToMetaMask}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Connect Wallet
</button>
) : (
<div>
<p>Connected as: {address}</p>
</div>
)}npm run dev/**
* @param somniaStreamsEventId The identifier of a registered event schema within Somnia streams protocol or null if using a custom event source
* @param ethCalls Fixed set of ETH calls that must be executed before onData callback is triggered. Multicall3 is recommended. Can be an empty array
* @param context Event sourced selectors to be added to the data field of ETH calls, possible values: topic0, topic1, topic2, topic3, topic4, data and address
* @param onData Callback for a successful reactivity notification
* @param onError Callback for a failed attempt
* @param eventContractSource Alternative contract event source (any on somnia) that will be emitting the logs specified by topicOverrides
* @param topicOverrides Optional when using Somnia streams as an event source but mandatory when using a different event source
* @param onlyPushChanges Whether the data should be pushed to the subscriber only if eth_call results are different from the previous
*/
export type SubscriptionInitParams = {
somniaStreamsEventId?: string
ethCalls: EthCall[]
context?: string
onData: (data: any) => void
onError?: (error: Error) => void
eventContractSource?: Address
topicOverrides?: Hex[]
onlyPushChanges: boolean
}
export interface StreamsInterface {
// Write
set(d: DataStream[]): Promise<Hex | null>;
emitEvents(e: EventStream[]): Promise<Hex | Error | null>;
setAndEmitEvents(d: DataStream[], e: EventStream[]): Promise<Hex | Error | null>;
// Manage
registerDataSchemas(registrations: DataSchemaRegistration[]): Promise<Hex | Error | null>;
registerEventSchemas(ids: string[], schemas: EventSchema[]): Promise<Hex | Error | null>;
manageEventEmittersForRegisteredStreamsEvent(
streamsEventId: string,
emitter: Address,
isEmitter: boolean
): Promise<Hex | Error | null>;
// Read
getByKey(schemaId: SchemaID, publisher: Address, key: Hex): Promise<Hex[] | SchemaDecodedItem[][] | null>;
getAtIndex(schemaId: SchemaID, publisher: Address, idx: bigint): Promise<Hex[] | SchemaDecodedItem[][] | null>;
getBetweenRange(
schemaId: SchemaID,
publisher: Address,
startIndex: bigint,
endIndex: bigint
): Promise<Hex[] | SchemaDecodedItem[][] | Error | null>;
getAllPublisherDataForSchema(
schemaReference: SchemaReference,
publisher: Address
): Promise<Hex[] | SchemaDecodedItem[][] | null>;
getLastPublishedDataForSchema(
schemaId: SchemaID,
publisher: Address
): Promise<Hex[] | SchemaDecodedItem[][] | null>;
totalPublisherDataForSchema(schemaId: SchemaID, publisher: Address): Promise<bigint | null>;
isDataSchemaRegistered(schemaId: SchemaID): Promise<boolean | null>;
computeSchemaId(schema: string): Promise<Hex | null>;
parentSchemaId(schemaId: SchemaID): Promise<Hex | null>;
schemaIdToId(schemaId: SchemaID): Promise<string | null>;
idToSchemaId(id: string): Promise<Hex | null>;
getAllSchemas(): Promise<string[] | null>;
getEventSchemasById(ids: string[]): Promise<EventSchema[] | null>;
// Helper
deserialiseRawData(
rawData: Hex[],
parentSchemaId: Hex,
schemaLookup: {
schema: string;
schemaId: Hex;
} | null
): Promise<Hex[] | SchemaDecodedItem[][] | null>;
// Subscribe
subscribe(initParams: SubscriptionInitParams): Promise<{ subscriptionId: string, unsubscribe: () => void } | undefined>;
// Protocol
getSomniaDataStreamsProtocolInfo(): Promise<GetSomniaDataStreamsProtocolInfoResponse | Error | null>;
}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)const sdk = new SDK({
public: getPublicClient(),
wallet: getWalletClient(),
})
const schemaId = await sdk.streams.computeSchemaId(gpsSchema)
console.log(`Schema ID ${schemaId}`)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" },
])const publishTxHash = await sdk.streams.set([{
id: toHex("london", { size: 32 }),
schemaId: computedGpsSchemaId,
data: encodedData,
}])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
}const data = await sdk.streams.getByKey(
computedGpsSchemaId,
publisherWalletAddress,
dataKey
)if (data) {
schemaEncoder.decode(data)
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract Greeter {
string public name;
address public owner;
event NameChanged(string oldName, string newName);
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can perform this action");
_;
}
constructor(string memory _initialName) {
name = _initialName;
owner = msg.sender;
}
function changeName(string memory _newName) external onlyOwner {
string memory oldName = name;
name = _newName;
emit NameChanged(oldName, _newName);
}
function greet() external view returns (string memory) {
return string(abi.encodePacked("Hello, ", name, "!"));
}
}{
"inputs": [ --->specifies it is an input, i.e. a WRITE function
{
"internalType": "string", ---> the data type
"name": "_newName", ---> params name
"type": "string" ---> data type
}
],
"name": "changeName", ---> function name
"outputs": [], ---> it does not have a return property
"stateMutability": "nonpayable", ---> It changes the Blockchain State without Token exchange, it simply stores information.
"type": "function" ---> It is a function.
},mkdir viem-example && cd viem-examplenpm init -ynpm i viemimport { createPublicClient, createWalletClient, http } from "viem";import { somniaTestnet } from "viem/chains"const publicClient = createPublicClient({
chain: somniaTestnet,
transport: http(),
}) export const ABI = [//...ABI here]import { ABI } from "./abi.js";const CONTRACT_ADDRESS = "0x2e7f682863a9dcb32dd298ccf8724603728d0edd";const interactWithContract = async () => {
try {
console.log("Reading message from the contract...");
// Read the "greet" function
const greeting = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: ABI,
functionName: "greet",
});
console.log("Current greeting:", greeting);
} catch (error) {
console.error("Error interacting with the contract:", error);
}
};
interactWithContract();node index.jsimport { privateKeyToAccount } from "viem/accounts";const walletClient = createWalletClient({
account: privateKeyToAccount($YOUR_PRIVATE_KEY),
chain: somniaTestnet,
transport: http(),
}); // Write to the "changeName" function
const txHash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: ABI,
functionName: "changeName",
args: ["Emmanuel!"],
});
console.log("Transaction sent. Hash:", txHash);
console.log("Waiting for transaction confirmation...");
// Wait for the transaction to be confirmed
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
console.log("Transaction confirmed. Receipt:", receipt);
// Read the updated "greet" function
const updatedGreeting = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: ABI,
functionName: "greet",
});
console.log("Updated greeting:", updatedGreeting);node index.js# Deploying a contract to Somnia Mainnet
npx hardhat run scripts/deploy.js --network somnia_mainnet# Deploying a contract to Somnia Testnet
npx hardhat run scripts/deploy.js --network somnia_testnet

address(0).import { SDK, SubscriptionCallback } from '@somnia-chain/reactivity';
const subscription = await sdk.subscribe({
ethCalls: [], // Optional: ETH view calls
onData: (data: SubscriptionCallback) => {
console.log('Event:', data);
},
// Other filters: eventTopics, origin, etc.
});
// Store subscription for later managementsubscription.unsubscribe();export type SoliditySubscriptionData = {
eventTopics?: Hex[]; // Optional filters
origin?: Address;
caller?: Address;
emitter?: Address;
handlerContractAddress: Address; // Required
handlerFunctionSelector?: Hex; // Optional override
priorityFeePerGas: bigint;
maxFeePerGas: bigint;
gasLimit: bigint;
isGuaranteed: boolean;
isCoalesced: boolean;
};
export type SoliditySubscriptionInfo = {
subscriptionData: SoliditySubscriptionData,
owner: Address
};const subData: SoliditySubscriptionData = {
handlerContractAddress: '0x123...',
priorityFeePerGas: parseGwei('2'),
maxFeePerGas: parseGwei('10'),
gasLimit: 2_000_000n,
isGuaranteed: true,
isCoalesced: false,
// Add filters as needed
};
const txHash = await sdk.createSoliditySubscription(subData);
if (txHash instanceof Error) {
console.error(txHash.message);
} else {
console.log('Created:', txHash);
}const info = await sdk.getSubscriptionInfo(123n); // bigint ID
if (info instanceof Error) {
console.error(info.message);
} else {
console.log('Info:', info);
}const txHash = await sdk.cancelSoliditySubscription(123n);
if (txHash instanceof Error) {
console.error(txHash.message);
} else {
console.log('Canceled:', txHash);
}ISomniaReactivityPrecompile.SubscriptionData memory subscriptionData = ISomniaReactivityPrecompile.SubscriptionData({
eventTopics: [Transfer.selector, bytes32(0), bytes32(0), bytes32(0)],
origin: address(0),
caller: address(0),
emitter: address(tokenAddress),
handlerContractAddress: address(this),
handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
priorityFeePerGas: 2 gwei, // 2 nanoSOMI
maxFeePerGas: 10 gwei, // 10 nanoSOMI
gasLimit: 2_000_000, // Sufficient for simple state updates
isGuaranteed: true,
isCoalesced: false
});
uint256 subscriptionId = somniaReactivityPrecompile.subscribe(subscriptionData);npx create-next-app@latest somnia-rainbowkit
cd somnia-rainbowkitnpm install wagmi viem @tanstack/react-query rainbowkit'use client';
import { WagmiProvider } from "wagmi";
import {
RainbowKitProvider,
getDefaultConfig,
} from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { somniaTestnet } from "viem/chains";
const queryClient = new QueryClient();
const config = getDefaultConfig({
appName: "Somnia Example App",
projectId: "Get_WalletConnect_ID",
chains: [somniaTestnet],
ssr: true,
});
export default function ClientProvider({ children }) {
return (
<WagmiConfig config={config}>
<QueryClientProvider client={queryClient}>
<RainbowkitProvider>{children}</RainbowkitProvider>
</QueryClientProvider>
</WagmiConfig>
);
}import ClientProvider from './components/ClientProvider';
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<title>Somnia DApp</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<ClientProvider>{children}</ClientProvider>
</body>
</html>
);
}'use client';
import { useAccount } from 'wagmi';
import { ConnectButton } from "@rainbow-me/rainbowkit";
export default function Home() {
const { address, isConnected } = useAccount();
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<ConnectButton />
{isConnected && (
<p className="mt-4 text-lg text-blue-600">Connected as: {address}</p>
)}
</main>
</div>
);
}npm run devnpm i @somnia-chain/reactivity viemimport { defineChain } from 'viem';
const somniaTestnet = defineChain({
id: 50312,
name: 'Somnia Testnet',
// ... full config as before
});import { SDK } from '@somnia-chain/reactivity';
import { createPublicClient, webSocket } from 'viem';
const publicClient = createPublicClient({
chain: somniaTestnet,
transport: webSocket(),
});
const sdk = new SDK({ public: publicClient });import { encodeFunctionData, erc20Abi, keccak256, toHex } from 'viem';
// Example: Transfer topic (keccak256('Transfer(address,address,uint256)'))
const transferTopic = keccak256(toHex('Transfer(address,address,uint256)'));
const ethCall = {
to: '0xExampleERC20Address', // Your target ERC20
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'balanceOf',
args: ['0xYourWalletAddress'], // Monitor this balance
}),
};
const filters = {
eventContractSources: ['0xExampleERC20Address'], // Filter to this emitter
topicOverrides: [transferTopic], // Only Transfer events
};const subscription = await sdk.subscribe({
ethCalls: [ethCall], // Bundled state query
...filters, // From Step 4
onlyPushChanges: true, // Efficient: Skip if balance unchanged
onData: (data) => {
console.log('Filtered Notification:', data);
// Decoding here (Step 6)
},
onError: (error) => {
console.error('Subscription Error:', error.message);
// Retry logic or alert
},
});
// Unsubscribe: subscription.unsubscribe();import { decodeEventLog, decodeFunctionResult } from 'viem';
// Inside onData:
const decodedLog = decodeEventLog({
abi: erc20Abi,
topics: data.result.topics,
data: data.result.data,
});
const decodedBalance = decodeFunctionResult({
abi: erc20Abi,
functionName: 'balanceOf',
data: data.result.simulationResults[0],
});
console.log('Decoded Transfer:', decodedLog.args); // { from, to, value }
console.log('New Balance:', decodedBalance);// Imports...
async function main() {
// Chain, client, SDK from Steps 2-3...
// ethCall and filters from Step 4...
const subscription = await sdk.subscribe({
ethCalls: [ethCall],
...filters,
onlyPushChanges: true,
onData: (data) => {
// Decoding from Step 6...
},
onError: (error) => console.error(error),
});
// Run indefinitely or unsubscribe on signal
}
main().catch(console.error);const info = await sdk.getSubscriptionInfo(subscriptionId);
console.log(JSON.stringify(info, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2));import { parseGwei } from 'viem';
await sdk.createSoliditySubscription({
handlerContractAddress: '0x...',
emitter: '0x...',
eventTopics: [eventSignature],
priorityFeePerGas: parseGwei('0'), // 0 nanoSOMI — typically, no priority is required
maxFeePerGas: parseGwei('10'), // 10 nanoSOMI — comfortable ceiling
gasLimit: 2_000_000n, // Sufficient for simple state updates
isGuaranteed: true,
isCoalesced: false,
});// WRONG — these are 10 wei and 20 wei, essentially zero
priorityFeePerGas: 10n,
maxFeePerGas: 20n,
// CORRECT — these are 2 nanoSOMI and 10 nanoSOMI
priorityFeePerGas: parseGwei('2'), // = 2_000_000_000n
maxFeePerGas: parseGwei('10'), // = 10_000_000_000n// RISKY — gas price fluctuates and dividing by 10 may yield too-low values
const gasPrice = await publicClient.getGasPrice();
priorityFeePerGas: gasPrice / 10n,
// SAFER — use fixed proven values
priorityFeePerGas: parseGwei('2'),// TOO LOW for any storage operations
gasLimit: 100_000n,
// SAFE for most handlers
gasLimit: 2_000_000n,
// SAFE for complex handlers with external calls
gasLimit: 10_000_000n,Deploy contract → address A
Create subscription → emitter: A, handler: A ✅
Redeploy contract → address B
Old subscription still points to A → ❌ won't work
Must create NEW subscription → emitter: B, handler: B ✅cost = gasUsed × effectiveGasPricecost ≈ 50,000 × 10 gwei = 500,000 gwei = 0.0005 SOMI per invocation// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract OpenGreeter {
string public name;
address public owner;
event NameChanged(string oldName, string newName);
constructor(string memory _initialName) {
name = _initialName;
owner = msg.sender;
}
function changeName(string memory _newName) public {
string memory oldName = name;
name = _newName;
emit NameChanged(oldName, _newName);
}
function greet() external view returns (string memory) {
return string(abi.encodePacked("Hello, ", name, "!"));
}
}npm thirdweb installmkdir somnia-thirdweb-example
cd somnia-thirdweb-examplenpx thirdweb deploy -k your_secret_key

uint32 number, string name, string abbreviation, string teamName, string teamColorint256 x, int256 y, int256 zconst { 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 = 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)
}









Build a Hello World program to understand Somnia Data Streams.
function payToAccess() external payable {
require(msg.value == 0.01 ether, "Must send exactly 0.01 SOMI");
}function withdraw() external onlyOwner {
payable(owner).transfer(address(this).balance);
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SOMIPayment {
address public owner;
constructor() {
owner = msg.sender;
}
// Modifier to restrict access to the contract owner
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this");
_;
}
// User must send exactly 0.01 SOMI to access this feature
function payToAccess() external payable {
require(msg.value == 0.01 ether, "Must send exactly 0.01 SOMI");
// Logic for access: mint token, grant download, emit event, etc.
}
// Withdraw collected SOMI to owner
function withdraw() external onlyOwner {
payable(owner).transfer(address(this).balance);
}
}constructor(address payable _seller) payable {
buyer = msg.sender;
seller = _seller;
amount = msg.value;
}function release() external onlyBuyer {
seller.transfer(amount);
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SOMIEscrow {
address public buyer;
address payable public seller;
uint256 public amount;
bool public isDeposited;
constructor(address payable _seller) payable {
buyer = msg.sender;
seller = _seller;
amount = msg.value;
require(amount > 0, "Must deposit SOMI");
isDeposited = true;
}
modifier onlyBuyer() {
require(msg.sender == buyer, "Only buyer can call this");
_;
}
function release() external onlyBuyer {
require(isDeposited, "No funds to release");
isDeposited = false;
seller.transfer(amount);
}
function refund() external onlyBuyer {
require(isDeposited, "No funds to refund");
isDeposited = false;
payable(buyer).transfer(amount);
}
}
receive() external payable {
emit Tipped(msg.sender, msg.value);
}function withdraw() external onlyOwner {
payable(owner).transfer(address(this).balance);
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SOMITipJar {
address public owner;
event Tipped(address indexed from, uint256 amount);
event Withdrawn(address indexed to, uint256 amount);
constructor() {
owner = msg.sender;
}
receive() external payable {
emit Tipped(msg.sender, msg.value);
}
function withdraw() external {
require(msg.sender == owner, "Only owner can withdraw");
uint256 balance = address(this).balance;
require(balance > 0, "No tips available");
payable(owner).transfer(balance);
emit Withdrawn(owner, balance);
}
}await walletClient.sendTransaction({
to: '0xTipJarAddress',
value: parseEther('0.05'),
});await sendTransaction({
to: contractAddress,
data: mintFunctionEncoded,
value: 0n, // user sends no SOMI
});npx create-next-app@latest somnia-subgraph-ui
cd somnia-subgraph-uinpm install thirdweb react-query graphqlNEXT_PUBLIC_SUBGRAPH_URL=https://proxy.somnia.chain.love/subgraphs/name/somnia-testnet/test-mytoken
NEXT_PUBLIC_SUBGRAPH_CLIENT_ID=YOUR_CLIENT_IDnpm run devimport { NextResponse } from "next/server";
const SUBGRAPH_URL = process.env.NEXT_PUBLIC_SUBGRAPH_URL as string;
const CLIENT_ID = process.env.NEXT_PUBLIC_SUBGRAPH_CLIENT_ID as string;
export async function POST(req: Request) {
try {
const body = await req.json();
const response = await fetch(SUBGRAPH_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Client-ID": CLIENT_ID, // ✅ Pass the Subgraph Client ID
},
body: JSON.stringify(body),
});
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Proxy Error:", error);
return NextResponse.json({ error: "Failed to fetch from Subgraph" }, { status: 500 });
}
}{
transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
id
from
to
value
blockTimestamp
transactionHash
}
}"use client";
import { useEffect, useState } from "react";
export default function TokenTransfers() {
// Store transfers in state
const [transfers, setTransfers] = useState<any[]>([]);
// Track loading state
const [loading, setLoading] = useState(true);
Next, we fetch the token transfer data when the component loads.
useEffect(() => {
async function fetchTransfers() {
setLoading(true); // Show loading state
const query = `
{
transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
id
from
to
value
blockTimestamp
transactionHash
}
}
`;
const response = await fetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const { data } = await response.json();
setTransfers(data.transfers || []); // Store results in state
setLoading(false); // Hide loading state
}
fetchTransfers();
}, []);return (
<div className="p-4">
<h2 className="text-xl font-semibold mb-4">Latest Token Transfers</h2>
{loading ? (
<p>Loading transfers...</p>
) : (
<ul>
{transfers.map((transfer) => (
<li key={transfer.id} className="mb-2 p-2 border rounded-lg">
<p><strong>From:</strong> {transfer.from}</p>
<p><strong>To:</strong> {transfer.to}</p>
<p><strong>Value:</strong> {parseFloat(transfer.value) / 1e18} STT</p>
<p>
<strong>TX:</strong>{" "}
<a
href={`https://shannon-explorer.somnia.network/tx/${transfer.transactionHash}`}
target="_blank"
className="text-blue-600 underline"
>
View Transaction
</a>
</p>
</li>
))}
</ul>
)}
</div>
);
}"use client";
import TokenTransfers from "../components/TokenTransfers";
export default function Home() {
return (
<main className="min-h-screen p-8">
<h1 className="text-2xl font-bold">Welcome to MyToken Dashboard</h1>
<TokenTransfers />
</main>
);
}npm run dev// 1. Call your contract function that emits the event
const tx = await contract.myFunction();
await tx.wait();
// 2. Poll for state change
for (let i = 0; i < 15; i++) {
await new Promise(r => setTimeout(r, 2000));
const result = await contract.myStateVariable();
if (result !== previousValue) {
console.log('Reactivity worked!');
break;
}
}event BlockTick(uint64 indexed blockNumber);
event EpochTick(uint64 indexed epochNumber, uint64 indexed blockNumber);
event Schedule(uint256 indexed timestampMillis);
const userSchema = `
uint64 timestamp,
string username,
string bio,
address owner
`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)Computed Schema ID: 0x5e4bce54a39b42b5b8a235b5d9e27e7031e39b65d7a42a6e0ac5e8b2c79e17b0
import { zeroBytes32 } from '@somnia-chain/streams'
const ignoreExistingSchemas = true
await sdk.streams.registerDataSchemas([
{ schemaName: "MySchema", schema: userSchema, parentSchemaId: zeroBytes32 }
], ignoreExistingSchemas)import { toHex } from 'viem'
const dataId = toHex(`username-${Date.now()}`, { size: 32 })
console.log("Data ID:", dataId)Data ID: 0x757365726e616d652d31373239303239323435import { SchemaEncoder } from '@somnia-chain/streams'
const encoder = new SchemaEncoder(userSchema)
const encodedData = encoder.encodeData([
{ name: 'timestamp', value: Date.now().toString(), type: 'uint64' },
{ name: 'username', value: 'Victory', type: 'string' },
{ name: 'bio', value: 'Blockchain Developer', type: 'string' },
{ name: 'owner', value: '0xYourWalletAddress', type: 'address' },
])
await sdk.streams.set([
{ id: dataId, schemaId, data: encodedData }
])const {
...
createWalletClient,
} = require("viem");
const { privateKeyToAccount } = require("viem/accounts");
// Create wallet client
const walletClient = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY),
chain: dreamChain,
transport: http(dreamChain.rpcUrls.default.http[0]),
});
// Initialize SDK
const sdk = new SDK({
...
wallet: walletClient,
});
const encodedData = schemaEncoder.encodeData([
...
{ name: "sender", value: wallet.account.address, type: "address" },
]);const messages = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)[
{ timestamp: 1729302920, username: "Victory", bio: "Blockchain Developer" }
]const chatSchema = `
uint64 timestamp,
bytes32 roomId,
string content,
string senderName,
address sender
`const schemaId = await sdk.streams.computeSchemaId(chatSchema)const dataId = toHex(`${roomName}-${Date.now()}`, { size: 32 })const encoded = encoder.encodeData([
{ name: 'timestamp', value: Date.now().toString(), type: 'uint64' },
{ name: 'roomId', value: toHex(roomName, { size: 32 }), type: 'bytes32' },
{ name: 'content', value: 'Hello world!', type: 'string' },
{ name: 'senderName', value: 'Victory', type: 'string' },
{ name: 'sender', value: publisherAddress, type: 'address' }
])
await sdk.streams.set([{ id: dataId, schemaId, data: encoded }])// mapping: schemaId => publisherAddress => dataId => data
mapping(bytes32 => mapping(address => mapping(bytes32 => bytes))) public dsstore;export const oraclePriceSchema = 'uint256 price, uint64 timestamp'import 'dotenv/config'
import { SDK, SchemaDecodedItem } from '@somnia-chain/streams'
import { publicClient } from '../lib/clients' // Assuming you have clients.ts from previous tutorial
import { oraclePriceSchema } from '../lib/schema'
import { Address, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
// --- Setup: Define our trusted and untrusted addresses ---
function getEnv(key: string): string {
const value = process.env[key]
if (!value) throw new Error(`Missing environment variable: ${key}`)
return value
}
// These are the addresses we trust for this schema.
// We get them from our .env file for this example.
const TRUSTED_ORACLES: Address[] = [
privateKeyToAccount(getEnv('PUBLISHER_1_PK') as `0x${string}`).address,
privateKeyToAccount(getEnv('PUBLISHER_2_PK') as `0x${string}`).address,
]
// This is a random, untrusted address.
const IMPOSTER_ORACLE: Address = '0x1234567890123456789012345678901234567890'
// --- Helper Functions ---
// Helper to decode the oracle data
function decodePriceRecord(row: SchemaDecodedItem[]): { price: bigint, timestamp: number } {
const val = (field: any) => field?.value?.value ?? field?.value ?? ''
return {
price: BigInt(val(row[0])),
timestamp: Number(val(r[1])),
}
}
/**
* Verification Utility
* Fetches data for a *single* publisher to verify its origin.
*/
async function verifyPublisher(sdk: SDK, schemaId: `0x${string}`, publisherAddress: Address) {
console.log(`\n--- Verifying Publisher: ${publisherAddress} ---`)
try {
const data = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)
if (!data || data.length === 0) {
console.log('[VERIFIED] No data found for this publisher.')
return
}
const records = (data as SchemaDecodedItem[][]).map(decodePriceRecord)
console.log(`[VERIFIED] Found ${records.length} record(s) cryptographically signed by this publisher:`)
records.forEach(record => {
console.log(` - Price: ${record.price}, Time: ${new Date(record.timestamp).toISOString()}`)
})
} catch (error: any) {
console.error(`Error during verification: ${error.message}`)
}
}
// --- Main Execution ---
async function main() {
const sdk = new SDK({ public: publicClient })
const schemaId = await sdk.streams.computeSchemaId(oraclePriceSchema)
if (!schemaId) throw new Error('Could not compute schemaId')
console.log('Starting Data Provenance Verification...')
console.log(`Schema: oraclePriceSchema (${schemaId})`)
// 1. Verify our trusted oracles
for (const oracleAddress of TRUSTED_ORACLES) {
await verifyPublisher(sdk, schemaId, oracleAddress)
}
// 2. Verify the imposter
// This will securely return NO data, even if the imposter
// published data to the same schemaId under their *own* address.
await verifyPublisher(sdk, schemaId, IMPOSTER_ORACLE)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})# Add to package.json
"verify": "ts-node src/scripts/verifyOrigin.ts"
# Run it
npm run verifyStarting Data Provenance Verification...
Schema: oraclePriceSchema (0x...)
--- Verifying Publisher: 0xPublisher1Address... ---
[VERIFIED] Found 2 record(s) cryptographically signed by this publisher:
- Price: 3200, Time: 2025-10-31T12:30:00.000Z
- Price: 3201, Time: 2025-10-31T12:31:00.000Z
--- Verifying Publisher: 0xPublisher2Address... ---
[VERIFIED] Found 1 record(s) cryptographically signed by this publisher:
- Price: 3199, Time: 2025-10-31T12:30:30.000Z
--- Verifying Publisher: 0x1234567890123456789012345678901234567890 ---
[VERIFIED] No data found for this publisher.ISomniaReactivityPrecompile.SubscriptionData
memory subscriptionData = ISomniaReactivityPrecompile
.SubscriptionData({
eventTopics: [BlockTick.selector, bytes32(0), bytes32(0), bytes32(0)],
emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
handlerContractAddress: address(this),
handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
/*...*/
});ISomniaReactivityPrecompile.SubscriptionData
memory subscriptionData = ISomniaReactivityPrecompile
.SubscriptionData({
eventTopics: [EpochTick.selector, bytes32(0), bytes32(0), bytes32(0)],
emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
handlerContractAddress: address(this),
handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
/*...*/
});ISomniaReactivityPrecompile.SubscriptionData
memory subscriptionData = ISomniaReactivityPrecompile
.SubscriptionData({
eventTopics: [EpochTick.selector, bytes32(uint256(42)), bytes32(0), bytes32(0)],
emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
handlerContractAddress: address(this),
handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
/*...*/
});ISomniaReactivityPrecompile.SubscriptionData
memory subscriptionData = ISomniaReactivityPrecompile
.SubscriptionData({
eventTopics: [Schedule.selector, 1794395471011, bytes32(0), bytes32(0)],
emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
handlerContractAddress: address(this),
handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
/*...*/
});npm i -D hardhat @nomicfoundation/hardhat-ethers ethers dotenv
npx hardhat # if project not initialized yet
cp .env.example .env || trueimport { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-ethers";
import * as dotenv from "dotenv";
dotenv.config();
const cfgFromEnv = {
mainnetUrl: process.env.SOMNIA_RPC_MAINNET || "https://api.infra.mainnet.somnia.network/",
testnetUrl: process.env.SOMNIA_RPC_TESTNET || "https://dream-rpc.somnia.network/",
forkMainnetBlock: process.env.FORK_BLOCK_MAINNET ? Number(process.env.FORK_BLOCK_MAINNET) : undefined,
forkTestnetBlock: process.env.FORK_BLOCK_TESTNET ? Number(process.env.FORK_BLOCK_TESTNET) : undefined,
};
const config: HardhatUserConfig = {
solidity: "0.8.19",
networks: {
hardhat: {
chainId: 31337,
},
localhost: {
url: "http://127.0.0.1:8545",
chainId: 31337,
},
somnia_testnet: {
url: cfgFromEnv.testnetUrl,
chainId: 50312,
},
somnia_mainnet: {
url: cfgFromEnv.mainnetUrl,
chainId: 5031,
},
},
};
export default config;hardhat: {
forking: {
url: process.env.SOMNIA_RPC_TESTNET!,
blockNumber: process.env.FORK_BLOCK_TESTNET ? Number(process.env.FORK_BLOCK_TESTNET) : undefined,
},
}# in-process fork
npx hardhat test
# persistent node
npx hardhat node --fork $SOMNIA_RPC_TESTNET ${FORK_BLOCK_TESTNET:+--fork-block-number $FORK_BLOCK_TESTNET}hardhat: {
forking: {
url: process.env.SOMNIA_RPC_MAINNET!,
blockNumber: process.env.FORK_BLOCK_MAINNET ? Number(process.env.FORK_BLOCK_MAINNET) : undefined,
},
}# in-process fork
npx hardhat test
# persistent node
npx hardhat node --fork $SOMNIA_RPC_MAINNET ${FORK_BLOCK_MAINNET:+--fork-block-number $FORK_BLOCK_MAINNET}anvil --fork-url $SOMNIA_RPC_TESTNET ${FORK_BLOCK_TESTNET:+--fork-block-number $FORK_BLOCK_TESTNET} --port 8546anvil --fork-url $SOMNIA_RPC_MAINNET ${FORK_BLOCK_MAINNET:+--fork-block-number $FORK_BLOCK_MAINNET} --port 8546# .env (example)
SOMNIA_RPC_MAINNET=https://api.infra.mainnet.somnia.network/
SOMNIA_RPC_TESTNET=https://dream-rpc.somnia.network/
# Optional: pin block numbers for reproducible forks
FORK_BLOCK_MAINNET=
FORK_BLOCK_TESTNET=// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Counter {
uint256 private count;
event CountedTo(uint256 number);
function getCount() public view returns (uint256) {
return count;
}
function increment() public {
count += 1;
emit CountedTo(count);
}
}import { expect } from "chai";
import { ethers } from "hardhat";
describe("Counter Contract", function () {
it("Should increment the count by 1", async function () {
const Counter = await ethers.getContractFactory("Counter");
const counter = await Counter.deploy();
await counter.waitForDeployment();
expect(await counter.getCount()).to.equal(0);
const tx = await counter.increment();
await tx.wait();
expect(await counter.getCount()).to.equal(1);
});
it("Should emit a CountedTo event", async function () {
const Counter = await ethers.getContractFactory("Counter");
const counter = await Counter.deploy();
await counter.waitForDeployment();
await expect(counter.increment()).to.emit(counter, "CountedTo").withArgs(1);
});
});# Run tests on the default in-process Hardhat network
npx hardhat test
# Or, start a local node in a separate terminal
npx hardhat node
# Then run tests against it
npx hardhat test --network localhostimport { ethers, network } from "hardhat";
async function main() {
const target = "0xYourSomniaAddress"; // account to impersonate
await network.provider.request({ method: "hardhat_impersonateAccount", params: [target] });
const signer = await ethers.getSigner(target);
await network.provider.send("hardhat_setBalance", [
target,
"0x152d02c7e14af6800000" // 1000 ether in wei
]);
console.log("Impersonating:", await signer.getAddress());
}
main().catch((e) => { console.error(e); process.exit(1); });await network.provider.send("evm_setNextBlockTimestamp", [Math.floor(Date.now()/1000) + 3600]);
await network.provider.send("evm_mine");const id = await network.provider.send("evm_snapshot");
// ... run actions ...
await network.provider.send("evm_revert", [id]);await network.provider.request({
method: "hardhat_reset",
params: [{ forking: { url: process.env.SOMNIA_RPC_TESTNET!, blockNumber: Number(process.env.FORK_BLOCK_TESTNET||0) || undefined } }]
});




import "dotenv/config";
import "@nomicfoundation/hardhat-verify";
import "@nomicfoundation/hardhat-ignition-ethers";
import { HardhatUserConfig } from "hardhat/config";
const config: HardhatUserConfig = {
solidity: "0.8.28",
networks: {
somnia: {
url: process.env.SOMNIA_RPC_HTTPS!,
accounts: [process.env.PRIVATE_KEY!],
},
},
sourcify: { enabled: false },
etherscan: {
apiKey: { somnia: process.env.SOMNIA_EXPLORER_API_KEY || "" },
customChains: [
{
network: "somnia",
chainId: 50312,
urls: {
apiURL: "https://shannon-explorer.somnia.network/api",
browserURL: "https://shannon-explorer.somnia.network",
},
},
],
},
};
export default config;
import { ethers } from "hardhat";
async function main() {
const contractAddr = "<DEPLOYED_ADDRESS>";
const nft = await ethers.getContractAt("NFTTest", contractAddr);
const owner = (await ethers.getSigners())[0];
// Example: 10 tokens at /ipfs/<CID>/{0..9}.json
const CID = "<YOUR_METADATA_CID>";
for (let i = 0; i < 10; i++) {
const uri = `/ipfs/${CID}/${i}.json`;
const tx = await nft.safeMint(owner.address, uri);
console.log(`Mint tx ${i}:`, tx.hash);
await tx.wait();
}
const lastId = await nft.callStatic.safeMint(owner.address, `/ipfs/${CID}/999.json`).catch(()=>null);
console.log("Minted 10 tokens. Next simulated ID (no state change):", lastId ?? "N/A");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";contract NFTTest is ERC721, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
...
}constructor(address initialOwner)
ERC721("NFTTest", "NFTT")
Ownable(initialOwner)
{}function _baseURI() internal pure override returns (string memory) {
return "https://ipfs.io";
}function safeMint(address to, string memory uri)
public
onlyOwner
returns (uint256)
{
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
return tokenId;
}function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}mkdir somnia-nft && cd somnia-nft
npm init -y
npm install --save-dev hardhat typescript ts-node @types/node
npx hardhat
npm install @openzeppelin/contracts
npm install --save-dev @nomicfoundation/hardhat-verify @nomicfoundation/hardhat-ignition @nomicfoundation/hardhat-ignition-ethersPRIVATE_KEY=0xYourPrivateKey
SOMNIA_RPC_HTTPS=https://dream-rpc.somnia.networkimport { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const NFTTestModule = buildModule("NFTTestModule", (m) => {
const initialOwner = m.getParameter("initialOwner", "0xYourOwnerAddress");
const nft = m.contract("NFTTest", [initialOwner]);
return { nft };
});
export default NFTTestModule;npx hardhat ignition deploy ignition/modules/NFTTest.ts --network somnianpx hardhat verify --network somnia <DEPLOYED_ADDRESS> 0xYourOwnerAddressnpx hardhat run scripts/mint.ts --network somniaimport { ethers } from "hardhat";
async function main() {
const contractAddr = "<DEPLOYED_ADDRESS>";
const nft = await ethers.getContractAt("NFTTest", contractAddr);
const uri = await nft.tokenURI(0);
console.log("tokenURI(0):", uri);
}
main().catch(console.error);npx hardhat run scripts/read-uri.ts --network somnianpm i @somnia-chain/streams viem dotenvPRIVATE_KEY=0xYOUR_PRIVATE_KEY
PUBLIC_KEY=0xYOUR_PUBLIC_ADDRESSconst { 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 };
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()
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();npm run publishernpm run subscriberSchema ID: 0x27c30fa6547c34518f2de6a268b29ac3b54e51c98f8d0ef6018bbec9153e9742
⚠️ Schema already registered. Continuing...
✅ Published: Hello World #1 (Tx: 0xf21ad71a6c7aa54c171ad38b79ef417e8488fd750ce00c1357918b7c7fa5c951)
✅ Published: Hello World #2 (Tx: 0xe999b0381ba9d937d85eb558fefe214fa4e572767c4e698c6e31588ff0e68f0a)🆕 Hello World #2 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:04 PM
🆕 Hello World #3 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:07 PM# Vulnerability Report — Somnia Bridge Contract
## Summary
Bridge contract mishandles token decimals in cross-chain conversion.
## Impact
Potential underflow on tokens with decimals < 18.
## Steps to Reproduce
1. Deploy ERC20 with 6 decimals.
2. Execute `bridgeToSomnia(token, 1000000)`.
3. Observe incorrect amount on destination.
## Expected vs Actual
Expected: normalized 1 token.
Actual: 0.000001 tokens received.
## Suggested Fix
Add decimal normalization logic.
## Proof of Concept
Testnet Tx: `0x92b...4fe1`
## Contact
@emreyeth (Telegram)# Vulnerability Report — Somnia Network
## Summary
Brief description of the issue.
## Impact
Potential risks if exploited.
## Steps to Reproduce
1. Step-by-step actions.
2. Include RPC endpoint, contract address, and network (Mainnet or Shannon Testnet).
## Expected vs Actual Behavior
Explain the difference in observed vs intended behavior.
## Proof of Concept (PoC)
Include transaction hash, minimal code snippet, or call trace.
## Suggested Fix (Optional)
Provide insights or improvement recommendations.
## Contact
Telegram / Discord handle / Email.npx hardhat init// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract BuyMeCoffee {
event CoffeeBought(
address indexed supporter,
uint256 amount,
string message,
uint256 timestamp
);
address public owner;
struct Contribution {
address supporter;
uint256 amount;
string message;
uint256 timestamp;
}
Contribution[] public contributions;
constructor() {
owner = msg.sender;
}
function buyCoffee(string memory message) external payable {
require(msg.value > 0, "Amount must be greater than zero.");
contributions.push(
Contribution(msg.sender, msg.value, message, block.timestamp)
);
emit CoffeeBought(msg.sender, msg.value, message, block.timestamp);
}
function withdraw() external {
require(msg.sender == owner, "Only the owner can withdraw funds.");
payable(owner).transfer(address(this).balance);
}
function getContributions() external view returns (Contribution[] memory) {
return contributions;
}
function setOwner(address newOwner) external {
require(msg.sender == owner, "Only the owner can set a new owner.");
owner = newOwner;
}
}npx hardhat compileCompiling...
Compiled 1 contract successfullyimport { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const BuyMeCoffee = buildModule("BuyMeCoffee", (m) => {
const contract = m.contract("BuyMeCoffee");
return { contract };
});
module.exports = BuyMeCoffee;module.exports = {
// ...
networks: {
somnia: {
url: "https://dream-rpc.somnia.network",
accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
},
},
// ...
};npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somniaimport { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.28",
networks: {
somnia: {
url: "https://dream-rpc.somnia.network",
accounts: ["YOUR_PRIVATE_KEY"],
},
},
sourcify: {
enabled: false,
},
etherscan: {
apiKey: {
somnia: "empty",
},
customChains: [
{
network: "somnia",
chainId: 50312,
urls: {
apiURL: "https://shannon-explorer.somnia.network/api",
browserURL: "https://shannon-explorer.somnia.network",
},
},
],
},
};npx hardhat verify --network somnia DEPLOYED_CONTRACT_ADDRESS "ConstructorArgument1" ...npx hardhat verify --network somnia 0xYourContractAddress "YourDeployerWalletAddress"


telemetry schema.+-------------+ publishData(payload) +--------------------+
| Publisher | --------------------------------> | Somnia Streams L1 |
| (wallet) | | (on-chain data) |
+-------------+ +--------------------+
^ |
| getAllPublisherDataForSchema
| v
+-------------+ +-----------+
| Subscriber | <------------------------------------ | Reader |
| (frontend) | | (SDK) |
+-------------+ +-----------+npm i @somnia-chain/streams viemRPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY// 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)// 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) })// lib/schema.ts
export const chatSchema =
'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'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()// 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()// 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()
npx create-next-app@latest somnia-thirdweb
cd somnia-thirdweb
npm install thirdweb ethers viem dotenvimport { createThirdwebClient } from "thirdweb";
export const client = createThirdwebClient({
clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string, // Replace with your actual Client ID
});NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your-client-id-here
NEXT_PUBLIC_SOMNIA_RPC_URL=https://dream-rpc.somnia.network/import { ThirdwebProvider } from 'thirdweb/react';
<body>
<ThirdwebProvider>
{children}
</ThirdwebProvider>
</body>
import { useActiveAccount } from "thirdweb/react";
const smartAccount = useActiveAccount();
<ConnectButton
client={client}
appMetadata={{
name: "Example App",
url: "https://example.com",
}}
/>{smartAccount ? (
<div className="mt-6 p-4 bg-white rounded-lg shadow">
<p className="text-lg font-semibold text-gray-700">
Connected as: {smartAccount.address}
</p>
{message && <p className="mt-2 text-green-600">{message}</p>}
</div>
) : (
<p className="text-lg text-red-600 text-center">
Please connect your wallet.
</p>
)}import { useSendTransaction } from "thirdweb/react";
import { ethers } from "ethers";
const { mutate: sendTransaction, isPending } = useSendTransaction();
const sendTokens = async () => {
if (!smartAccount) {
setMessage("No smart account connected.");
return;
}
console.log("Sending 0.01 STT from:", smartAccount.address);
sendTransaction(
{
to: "0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03",
value: ethers.parseUnits("0.01", 18),
chain: somniaTestnet,
client,
},
{
onSuccess: (receipt) => {
console.log("Transaction Success:", receipt);
setMessage(`Sent 0.01 STT! TX: ${receipt.transactionHash}`);
},
onError: (error) => {
console.error("Transaction Failed:", error);
setMessage("Transaction failed! Check console.");
},
}
);
};const [message, setMessage] = useState<string>('');
<button
onClick={sendTokens}
disabled={isPending}
className={`mt-4 px-6 py-2 rounded-lg ${
isPending ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{isPending ? 'Sending...' : 'Send 0.01 STT'}
</button>
{message && <p className='mt-2 text-green-600'>{message}</p>}'use client';
import { useState } from 'react';
import {
ConnectButton,
useActiveAccount,
useSendTransaction,
} from 'thirdweb/react';
import { ethers } from 'ethers';
import { client } from './client';
import { somniaTestnet } from 'viem/chain';
export default function Home() {
const [message, setMessage] = useState<string>('');
const smartAccount = useActiveAccount(); // Get connected account
const { mutate: sendTransaction, isPending } = useSendTransaction();
const sendTokens = async () => {
if (!smartAccount) {
setMessage('No smart account connected.');
return;
}
console.log('🚀 Sending 0.01 STT from:', smartAccount.address);
sendTransaction(
{
to: '0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03', // Replace
value: ethers.parseUnits('0.01', 18),
chain: somniaTestnet,
client,
},
{
onSuccess: (receipt) => {
console.log('Transaction Success:', receipt);
setMessage(`Sent 0.01 STT! TX: ${receipt.transactionHash}`);
},
onError: (error) => {
console.error('Transaction Failed:', error);
setMessage('Transaction failed! Check console.');
},
}
);
};
return (
<main className='p-4 pb-10 min-h-[100vh] flex items-center justify-center container max-w-screen-lg mx-auto'>
<div className='py-20'>
<div className='flex justify-center mb-10'>
<ConnectButton
client={client}
appMetadata={{
name: 'Example App',
url: 'https://example.com',
}}
/>
</div>
{smartAccount ? (
<div className='mt-6 p-4 bg-white rounded-lg shadow'>
<p className='text-lg font-semibold text-gray-700'>
Connected as: {smartAccount.address}
</p>
<button
onClick={sendTokens}
disabled={isPending}
className={`mt-4 px-6 py-2 rounded-lg ${
isPending
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{isPending ? 'Sending...' : 'Send 0.01 STT'}
</button>
{message && <p className='mt-2 text-green-600'>{message}</p>}
</div>
) : (
<p className='text-lg text-white-600 text-center'>
Please connect your wallet.
</p>
)}
</div>
</main>
);
}npm i @somnia-chain/reactivityimport { createPublicClient, createWalletClient, http, defineChain } from 'viem'
import { SDK } from '@somnia-chain/reactivity'
// Example: Public client (required for reading data)
const chain = defineChain() // see viem docs for defining a chain
const publicClient = createPublicClient({
chain,
transport: http(),
})
// Optional: Wallet client for writes
const walletClient = createWalletClient({
account,
chain,
transport: http(),
})
const sdk = new SDK({
public: publicClient,
wallet: walletClient, // Omit if not executing transactions on-chain
})import { SDK, WebsocketSubscriptionInitParams, SubscriptionCallback } from '@somnia-chain/reactivity'
const initParams: WebsocketSubscriptionInitParams = {
ethCalls: [], // State to read when events are emitted
onData: (data: SubscriptionCallback) => console.log('Received:', data),
}
const subscription = await sdk.subscribe(initParams)pragma solidity ^0.8.20;
import { SomniaEventHandler } from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
contract ExampleEventHandler is SomniaEventHandler {
function _onEvent(
address emitter,
bytes32[] calldata eventTopics,
bytes calldata data
) internal override {
// Execute your logic here
// Be careful about emitting events to avoid infinite loops
}
}import { SDK } from '@somnia-chain/reactivity';
import { parseGwei } from 'viem';
// Initialize the SDK
const sdk = new SDK({
public: publicClient,
wallet: walletClient,
})
// Create a Solidity subscription
// This is an example of a wildcard subscription to all events
// We do not need to supply SOMI—the chain ensures min balance
await sdk.createSoliditySubscription({
handlerContractAddress: '0x123...',
priorityFeePerGas: parseGwei('2'), // 2 gwei — minimum recommended for validators to process
maxFeePerGas: parseGwei('10'), // 10 gwei — max you're willing to pay (base + priority)
gasLimit: 2_000_000n, // Minimum recommended for state changes, increase for complex logic
isGuaranteed: true,
isCoalesced: false,
});mkdir somnia-aggregator
cd somnia-aggregator
npm init -y
npm i @somnia-chain/streams viem dotenv
npm i -D @types/node typescript ts-node{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}# .env
RPC_URL=https://dream-rpc.somnia.network/
# Simulates two different devices/publishers
PUBLISHER_1_PK=0xPUBLISHER_ONE_PRIVATE_KEY
PUBLISHER_2_PK=0xPUBLISHER_TWO_PRIVATE_KEYimport { 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)import 'dotenv/config'
import { createPublicClient, createWalletClient, http, PublicClient } from 'viem'
import { privateKeyToAccount, PrivateKeyAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'
function getEnv(key: string): string {
const value = process.env[key]
if (!value) {
throw new Error(`Missing environment variable: ${key}`)
}
return value
}
// A single Public Client for read operations
export const publicClient: PublicClient = createPublicClient({
chain: somniaTestnet,
transport: http(getEnv('RPC_URL')),
})
// Two different Wallet Clients for simulation
export const walletClient1 = createWalletClient({
account: privateKeyToAccount(getEnv('PUBLISHER_1_PK') as `0x${string}`),
chain: somniaTestnet,
transport: http(getEnv('RPC_URL')),
})
export const walletClient2 = createWalletClient({
account: privateKeyToAccount(getEnv('PUBLISHER_2_PK') as `0x${string}`),
chain: somniaTestnet,
transport: http(getEnv('RPC_URL')),
})// This schema will be used by multiple devices
export const telemetrySchema =
'uint64 timestamp, string deviceId, int32 x, int32 y, uint32 speed'import 'dotenv/config'
import { SDK, SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { publicClient, walletClient1, walletClient2 } from '../lib/clients'
import { telemetrySchema } from '../lib/schema'
import { toHex, Hex, WalletClient } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'
// Select which publisher to use
async function getPublisher(): Promise<{ client: WalletClient, deviceId: string }> {
const arg = process.argv[2] // 'p1' or 'p2'
if (arg === 'p2') {
console.log('Using Publisher 2 (Device B)')
return { client: walletClient2, deviceId: 'device-b-002' }
}
console.log('Using Publisher 1 (Device A)')
return { client: walletClient1, deviceId: 'device-a-001' }
}
// Helper function to encode the data
function encodeTelemetry(encoder: SchemaEncoder, deviceId: string): Hex {
const now = Date.now().toString()
return encoder.encodeData([
{ name: "timestamp", value: now, type: "uint64" },
{ name: "deviceId", value: deviceId, type: "string" },
{ name: "x", value: Math.floor(Math.random() * 1000).toString(), type: "int32" },
{ name: "y", value: Math.floor(Math.random() * 1000).toString(), type: "int32" },
{ name: "speed", value: Math.floor(Math.random() * 120).toString(), type: "uint32" },
])
}
async function main() {
const { client, deviceId } = await getPublisher()
const publisherAddress = client.account.address
console.log(`Publisher Address: ${publisherAddress}`)
const sdk = new SDK({ public: publicClient, wallet: client })
const encoder = new SchemaEncoder(telemetrySchema)
// 1. Compute the Schema ID
const schemaId = await sdk.streams.computeSchemaId(telemetrySchema)
if (!schemaId) throw new Error('Could not compute schemaId')
console.log(`Schema ID: ${schemaId}`)
// 2. Register Schema (Updated for new API)
console.log('Registering schema (if not already registered)...')
const ignoreAlreadyRegisteredSchemas = true
const regTx = await sdk.streams.registerDataSchemas([
{
schemaName: 'telemetry', // Updated: 'id' is now 'schemaName'
schema: telemetrySchema,
parentSchemaId: zeroBytes32
}
], ignoreAlreadyRegisteredSchemas)
if (regTx) {
console.log('Schema registration transaction sent:', regTx)
await waitForTransactionReceipt(publicClient, { hash: regTx })
console.log('Schema registered successfully!')
} else {
console.log('Schema was already registered. No transaction sent.')
}
// 3. Encode the data
const encodedData = encodeTelemetry(encoder, deviceId)
// 4. Publish the data
// We make the dataId unique with a timestamp and device ID
const dataId = toHex(`${deviceId}-${Date.now()}`, { size: 32 })
const txHash = await sdk.streams.set([
{ id: dataId, schemaId, data: encodedData }
])
if (!txHash) throw new Error('Failed to publish data')
console.log(`Publishing data... Tx: ${txHash}`)
await waitForTransactionReceipt(publicClient, { hash: txHash })
console.log('Data published successfully!')
}
main().catch((e) => {
console.error(e)
process.exit(1)
})"scripts": {
"publish:p1": "ts-node src/scripts/publishData.ts p1",
"publish:p2": "ts-node src/scripts/publishData.ts p2"
}# Terminal 1
npm run publish:p1
# Terminal 2
npm run publish:p2import 'dotenv/config'
import { SDK, SchemaDecodedItem } from '@somnia-chain/streams'
import { publicClient, walletClient1, walletClient2 } from '../lib/clients'
import { telemetrySchema } from '../lib/schema'
import { Address } from 'viem'
// LIST OF PUBLISHERS TO TRACK
// You could also fetch this list dynamically (e.g., from a contract or database).
const TRACKED_PUBLISHERS: Address[] = [
walletClient1.account.address,
walletClient2.account.address,
]
// Helper function to convert SDK data into a cleaner object
// (Similar to the 'val' function in the Minimal On-Chain Chat App Tutorial)
function decodeTelemetryRecord(row: SchemaDecodedItem[]): TelemetryRecord {
const val = (field: any) => field?.value?.value ?? field?.value ?? ''
return {
timestamp: Number(val(row[0])),
deviceId: String(val(row[1])),
x: Number(val(row[2])),
y: Number(val(row[3])),
speed: Number(val(row[4])),
}
}
// Type definition for our data
interface TelemetryRecord {
timestamp: number
deviceId: string
x: number
y: number
speed: number
publisher?: Address // We will add this field later
}
async function main() {
// The aggregator doesn't need to write data, so it only uses the publicClient
const sdk = new SDK({ public: publicClient })
const schemaId = await sdk.streams.computeSchemaId(telemetrySchema)
if (!schemaId) throw new Error('Could not compute schemaId')
console.log(`Aggregator started. Tracking ${TRACKED_PUBLISHERS.length} publishers...`)
console.log(`Schema ID: ${schemaId}\n`)
const allRecords: TelemetryRecord[] = []
// 1. Loop through each publisher
for (const publisherAddress of TRACKED_PUBLISHERS) {
console.log(`--- Fetching data for ${publisherAddress} ---`)
// 2. Fetch all data for the publisher based on the schema
// Note: The SDK automatically decodes the data if the schema is registered
const data = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)
if (!data || data.length === 0) {
console.log('No data found for this publisher.\n')
continue
}
// 3. Transform the data and add the 'publisher' field
const records: TelemetryRecord[] = (data as SchemaDecodedItem[][]).map(row => ({
...decodeTelemetryRecord(row),
publisher: publisherAddress // To know where the data came from
}))
console.log(`Found ${records.length} records.`)
// 4. Add all records to the main list
allRecords.push(...records)
}
// 5. Sort all data by timestamp
console.log('\n--- Aggregation Complete ---')
console.log(`Total records fetched: ${allRecords.length}`)
allRecords.sort((a, b) => a.timestamp - b.timestamp)
// 6. Display the result
console.log('\n--- Combined and Sorted Telemetry Log ---')
allRecords.forEach(record => {
console.log(
`[${new Date(record.timestamp).toISOString()}] [${record.publisher}] - Device: ${record.deviceId}, Speed: ${record.speed}`
)
})
}
main().catch((e) => {
console.error(e)
process.exit(1)
})"scripts": {
"publish:p1": "ts-node src/scripts/publishData.ts p1",
"publish:p2": "ts-node src/scripts/publishData.ts p2",
"aggregate": "ts-node src/scripts/aggregateData.ts"
}npm run aggregate/**
* @property The notification result data containing information about the event topic, data and view results
*/
export type SubscriptionCallback = {
result: {
topics: Hex[],
data: Hex,
simulationResults: Hex[]
}
}
/**
* @property ethCalls Fixed set of ETH calls that must be executed before onData callback is triggered. Multicall3 is recommended. Can be an empty array
* @property context Event sourced selectors to be added to the data field of ETH calls, possible values: topic0, topic1, topic2, topic3, data and address
* @property onData Callback for a successful reactivity notification
* @property onError Callback for a failed attempt
* @property eventContractSources Alternative contract event source(s) (any on somnia) that will be emitting the logs specified by topicOverrides
* @property topicOverrides Optional filter for specifying topics of interest, otherwise wildcard filter is applied (all events watched)
* @property onlyPushChanges Whether the data should be pushed to the subscriber only if eth_call results are different from the previous
*/
export type WebsocketSubscriptionInitParams = {
ethCalls: EthCall[]
context?: string
onData: (data: SubscriptionCallback) => void
onError?: (error: Error) => void
eventContractSources?: Address[]
topicOverrides?: Hex[]
onlyPushChanges?: boolean
}const subscription = await sdk.subscribe({
ethCalls: [], // State to read when events are emitted
onData: (data: SubscriptionCallback) => console.log('Received:', data),
})/**
* @property eventTopics Optional event filters
* @property origin Optional tx.origin filter
* @property caller Reserved for future use (not currently active in event matching)
* @property emitter Optional contract event emitter filter
* @property handlerContractAddress Contract that will handle subscription callback
* @property handlerFunctionSelector Optional override for specifying callback handler function
* @property priorityFeePerGas Additional priority fee that will be paid per gas consumed by callback
* @property maxFeePerGas Maximum fee per gas the subscriber is willing to pay (base fee + priority fee)
* @property gasLimit Maximum gas that will be provisioned per subscription callback
* @property isGuaranteed Whether event notification must be delivered regardless of block inclusion distance from emission
* @property isCoalesced Whether multiple events can be coalesced into a single handling call per block
*/
export type SoliditySubscriptionData = {
eventTopics?: Hex[];
origin?: Address;
caller?: Address;
emitter?: Address;
handlerContractAddress: Address;
handlerFunctionSelector?: Hex;
priorityFeePerGas: bigint;
maxFeePerGas: bigint;
gasLimit: bigint;
isGuaranteed: boolean;
isCoalesced: boolean;
}import { parseGwei } from 'viem';
await sdk.createSoliditySubscription({
handlerContractAddress: '0x123...',
priorityFeePerGas: parseGwei('2'), // 2 nanoSOMI — use parseGwei, not raw values
maxFeePerGas: parseGwei('10'), // 10 nanoSOMI
gasLimit: 2_000_000n, // Minimum recommended for state changes
isGuaranteed: true,
isCoalesced: false,
});export type SoliditySubscriptionInfo = {
subscriptionData: SoliditySubscriptionData,
owner: Address
}const subscriptionId = 1n;
const subscriptionInfo: SoliditySubscriptionInfo = await sdk.getSubscriptionInfo(
subscriptionId
);

Example of how to subscribe to all events and fetch state on Somnia using WebSockets off-chain
export default function Home() {
const { loginWithCrossAppAccount } = useCrossAppAccounts();
const { ready, authenticated, user, logout } = usePrivy();
const disableLogin = !ready || (ready && authenticated);
const [loginError, setLoginError] = useState<string | null>(null);
const [walletAddress, setWalletAddress] = useState<string | null>(null);
const providerAppId = 'cm8d9yzp2013kkr612h8ymoq8';
const startCrossAppLogin = async () => {
try {
setLoginError(null);
const result = await loginWithCrossAppAccount({
appId: providerAppId,
});
setWalletAddress(result.wallet?.address)
console.log(
'Logged in via global wallet:',
result,
);
} catch (err) {
console.warn('Cross-app login failed:', err);
setLoginError('Failed to log in with Global Wallet.');
}
};
......
{!ready ? (
<p>Loading...</p>
) : authenticated ? (
{walletAddress ? (
<p>Connected as: {walletAddress}</p>
) : (
<p className='text-gray-600'>No wallet address found.</p>
)}
<button
onClick={logout}
className='bg-red-600 text-white px-4 py-2 rounded'
>
Logout
</button>
</div>
) : (
<>
<button
onClick={startCrossAppLogin}
className='bg-purple-600 text-white px-4 py-2 rounded'
>
Login with Global Wallet
</button>
{loginError && <p className='text-red-500 text-sm'>{loginError}</p>}
</> </div>
)}
}'use client';
import {
usePrivy,
useCrossAppAccounts,
} from '@privy-io/react-auth';
import { useEffect, useState } from 'react';
import { createPublicClient, http, formatEther } from 'viem';
import { somniaTestnet } from 'viem/chains';
export default function Home() {
const { ready, authenticated, user, logout } = usePrivy();
const { loginWithCrossAppAccount, sendTransaction } = useCrossAppAccounts();
const [loginError, setLoginError] = useState<string | null>(null);
const [hydrated, setHydrated] = useState(false);
const [walletAddress, setWalletAddress] = useState<string | null>(null);
const [balance, setBalance] = useState<string>('');
const providerAppId = 'cm8d9yzp2013kkr612h8ymoq8';
const client = createPublicClient({
chain: somniaTestnet,
transport: http(),
});
const startCrossAppLogin = async () => {
try {
setLoginError(null);
const result = await loginWithCrossAppAccount({
appId: providerAppId,
});
console.log(
'Logged in via global wallet:',
result,
);
} catch (err) {
console.warn('Cross-app login failed:', err);
setLoginError('Failed to log in with Global Wallet.');
}
};
useEffect(() => {
if (authenticated) {
const globalWallet = user?.linkedAccounts?.find(
(account) =>
account.type === 'cross_app' &&
account.providerApp?.id === providerAppId
);
console.log(globalWallet);
const wallet = globalWallet?.smartWallets?.[0];
console.log(wallet);
if (wallet?.address) {
setWalletAddress(wallet.address);
setHydrated(true);
fetchBalance(wallet.address);
} else if (user?.wallet?.address) {
setWalletAddress(user.wallet.address);
setHydrated(true);
fetchBalance(user.wallet.address);
} else {
setHydrated(true);
}
}
}, [authenticated, user]);
const fetchBalance = async (address: string) => {
try {
const result = await client.getBalance({
address: address as `0x${string}`,
});
const formatted = parseFloat(formatEther(result)).toFixed(3);
setBalance(formatted);
} catch (err) {
console.error('Failed to fetch balance:', err);
}
};
const sendSTT = async () => {
if (!walletAddress) return;
console.log(walletAddress);
const txn = {
to: '0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03',
value: 1000000000000000,
chainId: 50312,
};
try {
const tx = await sendTransaction(txn, { address: walletAddress });
console.log('TX Sent:', tx);
if (walletAddress) fetchBalance(walletAddress);
} catch (err) {
console.error('TXN Failed:', err);
}
};
return (
<div className='grid min-h-screen items-center justify-items-center p-8 sm:p-20'>
<main className='flex flex-col gap-6 row-start-2 items-center'>
{!ready ? (
<p>Loading...</p>
) : !authenticated ? (
<>
<button
onClick={startCrossAppLogin}
className='bg-purple-600 text-white px-4 py-2 rounded'
>
Login with Global Wallet
</button>
{loginError && <p className='text-red-500 text-sm'>{loginError}</p>}
</>
) : hydrated ? (
<div className='space-y-4 text-center'>
{walletAddress ? (
<p>Connected as: {walletAddress}</p>
) : (
<p className='text-gray-600'>No wallet address found.</p>
)}
<p>Balance: {balance ? `${balance} STT` : 'Loading...'} </p>
<button
onClick={sendSTT}
className='bg-blue-600 text-white px-4 py-2 rounded'
>
Send 0.001 STT
</button>
<button
onClick={logout}
className='bg-red-600 text-white px-4 py-2 rounded'
>
Logout
</button>
</div>
) : (
<p>🔄 Logging in... Please wait</p>
)}
</main>
</div>
);
}npx create-next-app@latest somnia-privy
cd somnia-privynpm install @privy-io/react-auth viem'use client';
import { PrivyProvider } from '@privy-io/react-auth';
import { somniaTestnet } from 'viem/chains';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='en'>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
config={{
loginMethods: {
primary: ['email', 'google', 'privy:cm8d9yzp2013kkr612h8ymoq8'],
},
defaultChain: somniaTestnet,
supportedChains: [somniaTestnet],
embeddedWallets: {
createOnLogin: 'users-without-wallets',
},
}}
>
{children}
</PrivyProvider>
</body>
</html>
);
}NEXT_PUBLIC_PRIVY_APP_ID=your-privy-app-idimport { useCrossAppAccounts, usePrivy } from '@privy-io/react-auth'; const { sendTransaction } = useCrossAppAccounts();
......
const sendSTT = async () => {
if (!walletAddress) return;
const txn = {
to: '0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03',
value: 1000000000000000,
chainId: 50312,
};
try {
const tx = await sendTransaction(txn, { address: walletAddress });
console.log('TX Sent:', tx);
} catch (err) {
console.error('TXN Failed:', err);
}
};
......
<button onClick={sendSTT}> Send 0.001 STT</button>npm install -g @graphprotocol/graph-clinpm install --save-dev hardhat @nomicfoundation/hardhat-ignition-ethers @openzeppelin/contracts dotenv ethers// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply * 10**decimals());
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
export default buildModule("MyTokenModule", (m) => {
const initialSupply = m.getParameter("initialSupply", 1000000n * 10n ** 18n);
const myToken = m.contract("MyToken", [initialSupply]);
return { myToken };
});module.exports = {
// ...
networks: {
somniaTestnet: {
url: "https://dream-rpc.somnia.network",
accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
},
},
// ...
};
npx hardhat ignition deploy ./ignition/modules/MyTokenModule.ts --network somniaTestnet
require("dotenv").config();
const { ethers } = require("hardhat");
async function main() {
// Connect to Somnia RPC
const provider = new ethers.JsonRpcProvider(process.env.SOMNIA_RPC_URL);
// Load wallets from .env
const deployer = new ethers.Wallet(process.env.PRIVATE_KEY_1, provider);
const user1 = new ethers.Wallet(process.env.PRIVATE_KEY_2, provider);
const user2 = new ethers.Wallet(process.env.PRIVATE_KEY_3, provider);
const user3 = new ethers.Wallet(process.env.PRIVATE_KEY_4, provider);
const user4 = new ethers.Wallet(process.env.PRIVATE_KEY_5, provider);
const contractAddress = "0xBF9516ADc5263d277E2505d4e141F7159B103d33"; // Replace with your deployed contract address
const abi = [
"function transfer(address to, uint256 amount) external returns (bool)",
"function mint(address to, uint256 amount) external",
"function burn(uint256 amount) external",
];
// Attach to the deployed ERC20 contract
const token = new ethers.Contract(contractAddress, abi, provider);
console.log("🏁 Starting Token Transactions Simulation on Somnia...");
// Simulate Transfers
const transfers = [
{ from: deployer, to: user1.address, amount: "1000" },
{ from: deployer, to: user2.address, amount: "1000" },
{ from: user1, to: user2.address, amount: "50" },
{ from: user2, to: user3.address, amount: "30" },
{ from: user3, to: user4.address, amount: "10" },
{ from: user4, to: deployer.address, amount: "5" },
{ from: deployer, to: user2.address, amount: "100" },
{ from: user1, to: user3.address, amount: "70" },
{ from: user2, to: user4.address, amount: "40" },
];
for (const tx of transfers) {
const { from, to, amount } = tx;
const txResponse = await token.connect(from).transfer(to, ethers.parseUnits(amount, 18));
await txResponse.wait();
console.log(`✅ ${from.address} sent ${amount} MTK to ${to}`);
}
// Simulate Minting
const mintAmount1 = ethers.parseUnits("500", 18);
const mintTx1 = await token.connect(deployer).mint(user1.address, mintAmount1);
await mintTx1.wait();
console.log(`✅ Minted ${ethers.formatUnits(mintAmount1, 18)} MTK to User1!`);
const mintAmount2 = ethers.parseUnits("300", 18);
const mintTx2 = await token.connect(deployer).mint(user2.address, mintAmount2);
await mintTx2.wait();
console.log(`✅ Minted ${ethers.formatUnits(mintAmount2, 18)} MTK to User2!`);
// Simulate Burning
const burnAmount1 = ethers.parseUnits("50", 18);
const burnTx1 = await token.connect(user1).burn(burnAmount1);
await burnTx1.wait();
console.log(`🔥 User1 burned ${ethers.formatUnits(burnAmount1, 18)} MTK!`);
const burnAmount2 = ethers.parseUnits("100", 18);
const burnTx2 = await token.connect(user2).burn(burnAmount2);
await burnTx2.wait();
console.log(`🔥 User2 burned ${ethers.formatUnits(burnAmount2, 18)} MTK!`);
console.log("🏁 Simulation Complete on Somnia!");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});SOMNIA_RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY_1=0x...
PRIVATE_KEY_2=0x...
PRIVATE_KEY_3=0x...
PRIVATE_KEY_4=0x...
PRIVATE_KEY_5=0x...node scripts/interact.jsgraph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytoken{
"somnia-testnet": {
"network": "Somnia Testnet",
"rpc": "https://dream-rpc.somnia.network",
"startBlock": 12345678
}
}type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes!
to: Bytes!
value: BigInt!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}graph codegen
graph buildgraph deploy --node https://proxy.somnia.chain.love/graph/somnia-testnet --version-label 0.0.1 somnia-testnet/test-mytoken
--access-token=your_token_from_somnia_chain_love{
transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
id
from
to
value
blockTimestamp
transactionHash
}
}{
transfers(where: { from: "0xUserWalletAddress" }) {
id
from
to
value
}
}{
transfers(where: { blockTimestamp_gte: "1700000000", blockTimestamp_lte: "1710000000" }) {
id
from
to
value
}
}


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract SimpleNFT is ERC721 {
uint256 private _tokenIdCounter;
constructor() ERC721("SimpleNFT", "SNFT") {}
function mint(address to) public {
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter++;
_safeMint(to, tokenId);
}
function totalSupply() public view returns (uint256) {
return _tokenIdCounter;
}
}import { createThirdwebClient, getContract } from 'thirdweb';
import { SmartWalletOptions, inAppWallet } from 'thirdweb/wallets';
import { somniaTestnet } from 'thirdweb/chains';
// Validate environment variables
const clientId = process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID;
if (!clientId) {
throw new Error('No client ID provided');
}
// Initialize Thirdweb client
export const client = createThirdwebClient({
clientId: clientId,
});
// Use Somnia testnet
export const chain = somniaTestnet;
// Your deployed NFT contract address
export const nftContractAddress = '0x...'; // UPDATE with your contract address
// Get contract instance
export const nftContract = getContract({
address: nftContractAddress,
chain,
client,
});
// Account Abstraction configuration for standard wallet connection
export const accountAbstraction: SmartWalletOptions = {
chain,
sponsorGas: true, // Enable gasless transactions
};
// Smart Account infrastructure addresses
const FACTORY_ADDRESS = '0x4be0ddfebca9a5a4a617dee4dece99e7c862dceb'; // Thirdweb Account Factory
// In-app wallet configuration with smart accounts
export const wallets = [
inAppWallet({
smartAccount: {
chain: somniaTestnet,
sponsorGas: true,
factoryAddress: FACTORY_ADDRESS,
},
}),
];'use client';
import { useState } from 'react';
import { balanceOf, totalSupply } from 'thirdweb/extensions/erc721';
import {
ConnectButton,
TransactionButton,
useActiveAccount,
useReadContract,
} from 'thirdweb/react';
import { prepareContractCall } from 'thirdweb';
import {
accountAbstraction,
client,
nftContract,
wallets,
} from '../constants';
import Link from 'next/link';export default function Home() {
// Get the currently connected account (either smart account or regular wallet)
const account = useActiveAccount();
// State for showing transaction progress
const [txStatus, setTxStatus] = useState<string>('');
// Read the total number of NFTs minted from the contract
const { data: totalMinted } = useReadContract(totalSupply, {
contract: nftContract,
});
// Read how many NFTs the connected user owns
const { data: userBalance } = useReadContract(balanceOf, {
contract: nftContract,
owner: account?.address!,
queryOptions: { enabled: !!account }, // Only fetch when account is connected
});'use client';
import { useState } from 'react';
import { balanceOf, totalSupply } from 'thirdweb/extensions/erc721';
import {
ConnectButton,
TransactionButton,
useActiveAccount,
useReadContract,
} from 'thirdweb/react';
import { prepareContractCall } from 'thirdweb';
import {
accountAbstraction,
client,
nftContract,
wallets,
} from '../../constants';
import Link from 'next/link';
const GaslessHome: React.FC = () => {
const account = useActiveAccount();
const [txStatus, setTxStatus] = useState<string>('');
// Get total supply
const { data: totalMinted } = useReadContract(totalSupply, {
contract: nftContract,
});
// Get user's balance
const { data: userBalance } = useReadContract(balanceOf, {
contract: nftContract,
owner: account?.address!,
queryOptions: { enabled: !!account },
});
return (
<div className='flex flex-col items-center min-h-screen p-8'>
{/* Main Title */}
<h1 className='text-2xl md:text-6xl font-semibold md:font-bold tracking-tighter mb-12 text-zinc-100'>
Gasless NFT Minting on Somnia
</h1>
{/* Wallet Connection Button */}
<ConnectButton
client={client}
wallets={wallets} // Enables in-app wallet options
accountAbstraction={accountAbstraction} // Enables smart accounts for regular wallets
connectModal={{
size: 'wide',
title: 'Choose Your Login Method',
welcomeScreen: {
title: 'Gasless NFT Minting',
subtitle: 'Sign in to mint NFTs without gas fees',
},
}}
appMetadata={{
name: 'Somnia NFT Minter',
url: 'https://somnia.network',
}}
/>
{/* NFT Display and Minting Section */}
<div className='flex flex-col mt-8 items-center'>
{/* Stats Card */}
<div className='mb-8 p-8 bg-zinc-900 rounded-2xl shadow-xl'>
<div className='text-center mb-6'>
<p className='text-3xl font-bold text-white mb-2'>
{totalMinted?.toString() || '0'}
</p>
<p className='text-sm text-zinc-400'>Total NFTs Minted</p>
</div>
{/* NFT Visual Representation */}
<div className='flex items-center justify-center'>
<div className='w-48 h-48 bg-gradient-to-br from-purple-600 to-blue-600 rounded-xl flex items-center justify-center shadow-lg'>
<div className='text-white text-center'>
<p className='text-6xl font-bold mb-2'>NFT</p>
<p className='text-sm opacity-80'>SimpleNFT on Somnia</p>
</div>
</div>
</div>
</div>
{/* Conditional Rendering: Connected vs Not Connected */}
{account ? (
<div className='flex flex-col items-center gap-4'>
{/* User Stats */}
<div className='text-center'>
<p className='font-semibold text-lg'>
You own{' '}
<span className='text-green-400'>
{userBalance?.toString() || '0'}
</span>{' '}
NFTs
</p>
<p className='text-sm text-zinc-400 mt-1'>
Wallet: {account.address.slice(0, 6)}...{account.address.slice(-4)}
</p>
</div>
{/* Transaction Status */}
{txStatus && (
<p className='text-sm text-yellow-400 mb-2'>{txStatus}</p>
)}
{/* Mint Button */}
<TransactionButton
transaction={() =>
prepareContractCall({
contract: nftContract,
method: 'function mint(address to)',
params: [account.address],
})
}
onError={(error) => {
console.error('Transaction error:', error);
setTxStatus('');
// User-friendly error messages
let errorMessage = 'Transaction failed';
if (error.message?.includes('insufficient funds')) {
errorMessage = 'Insufficient funds for gas';
} else if (error.message?.includes('rejected')) {
errorMessage = 'Transaction rejected by user';
} else if (error.message?.includes('500')) {
errorMessage = 'Service temporarily unavailable';
}
alert(`Error: ${errorMessage}`);
}}
onTransactionSent={(result) => {
console.log('Transaction sent:', result.transactionHash);
setTxStatus('Transaction submitted! Waiting for confirmation...');
}}
onTransactionConfirmed={async (receipt) => {
console.log('Transaction confirmed:', receipt);
setTxStatus('');
alert('NFT minted successfully!');
}}
className='px-8 py-4 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 rounded-lg font-semibold transition-all transform hover:scale-105 shadow-lg'
>
Mint NFT (Gasless)
</TransactionButton>
</div>
) : (
{/* Not Connected State */}
<p className='text-center w-full mt-10 text-zinc-400'>
Connect your wallet to mint NFTs without gas fees!
</p>
)}
</div>
{/* Navigation - Removed since this is now the home page */}
</div>
);
};import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThirdwebProvider } from "thirdweb/react";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Gasless NFT Minting on Somnia",
description: "Mint NFTs without gas fees using Account Abstraction",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<ThirdwebProvider>
<main className="min-h-screen bg-zinc-950 text-white">
{children}
</main>
</ThirdwebProvider>
</body>
</html>
);
}npx create-next-app@latest somnia-gasless-nft --typescript --tailwind --app
cd somnia-gasless-nftnpm install thirdwebNEXT_PUBLIC_THIRDWEB_CLIENT_ID=your_client_id_herenpm i @somnia-chain/reactivity viemimport { defineChain } from 'viem';
const somniaTestnet = defineChain({
id: 50312,
name: 'Somnia Testnet',
network: 'testnet',
nativeCurrency: {
decimals: 18,
name: 'STT',
symbol: 'STT',
},
rpcUrls: {
default: {
http: ['https://dream-rpc.somnia.network'],
webSocket: ['ws://api.infra.testnet.somnia.network/ws'],
},
public: {
http: ['https://dream-rpc.somnia.network'],
webSocket: ['ws://api.infra.testnet.somnia.network/ws'],
},
},
});import { SDK } from '@somnia-chain/reactivity';
import { createPublicClient, webSocket } from 'viem';
const publicClient = createPublicClient({
chain: somniaTestnet,
transport: webSocket(),
});
const sdk = new SDK({ public: publicClient });import { encodeFunctionData, erc721Abi } from 'viem';
const ethCall = {
to: '0x23B66B772AE29708a884cca2f9dec0e0c278bA2c', // Example Somnia ERC721 contract
data: encodeFunctionData({
abi: erc721Abi,
functionName: 'balanceOf',
args: ['0x3dC360e0389683cA0341a11Fc3bC26252b5AF9bA'], // Example owner address
}),
};const subscription = await sdk.subscribe({
ethCalls: [ethCall], // Array of calls; add more if needed
onData: (data) => {
console.log('Raw Notification:', data);
// Decoding happens here (Step 6)
},
});import { decodeEventLog, decodeFunctionResult, erc20Abi } from 'viem';
// Inside onData:
const decodedLog = decodeEventLog({
abi: erc20Abi, // Or your custom ABI
topics: data.result.topics,
data: data.result.data,
});
const decodedFunctionResult = decodeFunctionResult({
abi: erc721Abi, // Match the call's ABI
functionName: 'balanceOf',
data: data.result.simulationResults[0], // First call's result
});
console.log('Decoded Event:', decodedLog); // e.g., { eventName: 'Transfer', args: { from, to, value } }
console.log('Decoded Balance:', decodedFunctionResult); // e.g., 42n// Imports from above...
async function main() {
// Chain, client, SDK setup from Steps 2-3...
// EthCall from Step 4...
const subscription = await sdk.subscribe({
ethCalls: [ethCall],
onData: (data) => {
// Decoding from Step 6...
},
});
// Keep running (e.g., for a server) or unsubscribe after testing
}
main().catch(console.error);sdk.streams.getAllPublisherDataForSchema()Build a Tap-to-Play Onchain Game
import { DIAOracleLib } from "./libraries/DIAOracleLib.sol";function getPrice(
address oracle,
string memory key
)
public
view
returns (uint128 latestPrice, uint128 timestampOflatestPrice);function getPriceIfNotOlderThan(
address oracle,
string memory key,
uint128 maxTimePassed
)
public
view
returns (uint128 price, bool inTime)
{// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
interface IDIAOracleV2 {
function getValue(string memory) external view returns (uint128,
uint128);
}
contract DIAOracleSample {
address diaOracle;
constructor(address _oracle) {
diaOracle = _oracle;
}
function getPrice(string memory key)
external
view
returns (
uint128 latestPrice,
uint128 timestampOflatestPrice
) {
(latestPrice, timestampOflatestPrice) =
IDIAOracleV2(diaOracle).getValue(key);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract DAO {
struct Proposal {
string description; // Proposal details
uint256 deadline; // Voting deadline
uint256 yesVotes; // Votes in favor
uint256 noVotes; // Votes against
bool executed; // Whether the proposal has been executed
address proposer; // Address of the proposer
}
mapping(uint256 => Proposal) public proposals;
mapping(address => uint256) public votingPower;
mapping(uint256 => mapping(address => bool)) public hasVoted;
uint256 public totalProposals;
uint256 public votingDuration = 10 minutes;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
constructor() {
owner = msg.sender;
}
function deposit() external payable {
require(msg.value == 0.001 ether, "Must deposit STT");
votingPower[msg.sender] += msg.value;
}
function createProposal(string calldata description) external {
require(votingPower[msg.sender] > 0, "No voting power");
proposals[totalProposals] = Proposal({
description: description,
deadline: block.timestamp + votingDuration,
yesVotes: 0,
noVotes: 0,
executed: false,
proposer: msg.sender
});
totalProposals++;
}
function vote(uint256 proposalId, bool support) external {
Proposal storage proposal = proposals[proposalId];
require(block.timestamp < proposal.deadline, "Voting has ended");
require(!hasVoted[proposalId][msg.sender], "Already voted");
require(votingPower[msg.sender] > 0, "No voting power");
hasVoted[proposalId][msg.sender] = true;
if (support) {
proposal.yesVotes += votingPower[msg.sender];
} else {
proposal.noVotes += votingPower[msg.sender];
}
}
function executeProposal(uint256 proposalId) external {
Proposal storage proposal = proposals[proposalId];
require(block.timestamp >= proposal.deadline, "Voting still active");
require(!proposal.executed, "Proposal already executed");
require(proposal.yesVotes > proposal.noVotes, "Proposal did not pass");
proposal.executed = true;
// Logic for proposal execution
// Example: transfer STT to proposer as a reward for successful vote pass
payable(proposal.proposer).transfer(0.001 ether);
}
}
mapping(uint256 => Proposal) public proposals;struct Proposal {
string description; // Proposal details
uint256 deadline; // Voting deadline
uint256 yesVotes; // Votes in favor
uint256 noVotes; // Votes against
bool executed; // Whether the proposal has been executed
address proposer; // Address of the proposer
}mapping(address => uint256) public votingPower;mapping(uint256 => mapping(address => bool)) public hasVoted;constructor() {
owner = msg.sender;
}function deposit() external payable {
require(msg.value >= 0.001 ether, "Minimum deposit is 0.001 STT");
votingPower[msg.sender] += msg.value;
}function createProposal(string calldata description) external {
require(votingPower[msg.sender] > 0, "No voting power");
proposals[totalProposals] = Proposal({
description: description,
deadline: block.timestamp + votingDuration,
yesVotes: 0,
noVotes: 0,
executed: false,
proposer: msg.sender
});
totalProposals++;
} function vote(uint256 proposalId, bool support) external {
Proposal storage proposal = proposals[proposalId];
require(block.timestamp < proposal.deadline, "Voting has ended");
require(!hasVoted[proposalId][msg.sender], "Already voted");
require(votingPower[msg.sender] > 0, "No voting power");
hasVoted[proposalId][msg.sender] = true;
if (support) {
proposal.yesVotes += votingPower[msg.sender];
} else {
proposal.noVotes += votingPower[msg.sender];
}
}function executeProposal(uint256 proposalId) external {
Proposal storage proposal = proposals[proposalId];
require(block.timestamp >= proposal.deadline, "Voting still active");
require(!proposal.executed, "Proposal already executed");
require(proposal.yesVotes > proposal.noVotes, "Proposal did not pass");
proposal.executed = true;
payable(proposal.proposer).transfer(0.001 ether);
}uint256 public totalProposals;uint256 public votingDuration = 10 minutes;address public owner;import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const dao = buildModule("DAO", (m) => {
const contract = m.contract("DAO");
return { contract };
});
module.exports = dao;const config = {
solidity: "0.8.28",
networks: {
somnia: {
url: "https://dream-rpc.somnia.network",
accounts: ["YOUR_PRIVATE_KEY"],
},
},
};npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somniaawait dao.deposit({ value: ethers.utils.parseEther("0.001") });await dao.createProposal("Fund development of new feature");await dao.vote(0, true); // Vote ‘yes’ on proposal 0await dao.executeProposal(0);const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("DAO", function () {
let dao;
let owner, addr1;
beforeEach(async function () {
const DAO = await ethers.getContractFactory("DAO");
dao = await DAO.deploy();
[owner, addr1] = await ethers.getSigners();
});
it("Should allow deposits and update voting power", async function () {
await dao.connect(addr1).deposit({ value: ethers.utils.parseEther("0.001") });
expect(await dao.votingPower(addr1.address)).to.equal(ethers.utils.parseEther("0.001"));
});
it("Should allow proposal creation", async function () {
await dao.connect(addr1).deposit({ value: ethers.utils.parseEther("0.001") });
await dao.connect(addr1).createProposal("Test Proposal");
const proposal = await dao.proposals(0);
expect(proposal.description).to.equal("Test Proposal");
});
});npx hardhat test// Schema: 'uint64 timestamp, address player, uint256 score'
export const leaderboardSchema =
'uint64 timestamp, address player, uint256 score'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// A simplified interface for the Somnia Streams contract
interface IStreams {
struct DataStream {
bytes32 id;
bytes32 schemaId;
bytes data;
}
// This is the correct low-level function name
function esstores(DataStream[] calldata streams) external;
}
/**
* @title GameLeaderboard
* This contract is a DApp Publisher Proxy.
* Users call submitScore() here.
* This contract then calls somniaStreams.esstores() as a single publisher.
*/
contract GameLeaderboard {
IStreams public immutable somniaStreams;
bytes32 public immutable leaderboardSchemaId;
event ScoreSubmitted(address indexed player, uint256 score);
/**
* @param _streamsAddress The deployed address of the Somnia Streams contract
* (e.g., 0x6AB397FF662e42312c003175DCD76EfF69D048Fc on Somnia Testnet).
* @param _schemaId The pre-computed schemaId for 'uint64 timestamp, address player, uint256 score'.
*/
constructor(address _streamsAddress, bytes32 _schemaId) {
somniaStreams = IStreams(_streamsAddress);
leaderboardSchemaId = _schemaId;
}
/**
* @notice Players call this function to submit their score.
* @param score The player's score.
*/
function submitScore(uint256 score) external {
// 1. Get the original publisher's address
address player = msg.sender;
uint64 timestamp = uint64(block.timestamp);
// 2. Encode the data payload to match the schema
// Schema: 'uint64 timestamp, address player, uint256 score'
bytes memory data = abi.encode(timestamp, player, score);
// 3. Create a unique dataId (e.g., hash of player and time)
bytes32 dataId = keccak256(abi.encodePacked(player, timestamp));
// 4. Prepare the DataStream struct
IStreams.DataStream[] memory d = new IStreams.DataStream[](1);
d[0] = IStreams.DataStream({
id: dataId,
schemaId: leaderboardSchemaId,
data: data
});
// 5. Call Somnia Streams. The `msg.sender` for this call
// is THIS contract (GameLeaderboard).
somniaStreams.esstores(d);
// 6. Emit a DApp-specific event for good measure
emit ScoreSubmitted(player, score);
}
}
import 'dotenv/config'
import { createWalletClient, http, createPublicClient, parseAbi } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from '../lib/chain' // From previous tutorials
import { waitForTransactionReceipt } from 'viem/actions'
// --- DApp Contract Setup ---
// This is the address you get after deploying GameLeaderboard.sol
const DAPP_CONTRACT_ADDRESS = '0x...' // Your deployed GameLeaderboard contract address
// A minimal ABI for our GameLeaderboard contract
const DAPP_ABI = parseAbi([
'function submitScore(uint256 score) external',
])
// --- --- ---
function getEnv(key: string): string {
const value = process.env[key]
if (!value) throw new Error(`Missing environment variable: ${key}`)
return value
}
// We can use any publisher wallet
const walletClient = createWalletClient({
account: privateKeyToAccount(getEnv('PUBLISHER_1_PK') as `0x${string}`),
chain: somniaTestnet,
transport: http(getEnv('RPC_URL')),
})
const publicClient = createPublicClient({
chain: somniaTestnet,
transport: http(getEnv('RPC_URL')),
})
async function main() {
const newScore = Math.floor(Math.random() * 10000)
console.log(`Player ${walletClient.account.address} submitting score: ${newScore}...`)
try {
const { request } = await publicClient.simulateContract({
account: walletClient.account,
address: DAPP_CONTRACT_ADDRESS,
abi: DAPP_ABI,
functionName: 'submitScore',
args: [BigInt(newScore)],
})
const txHash = await walletClient.writeContract(request)
console.log(`Transaction sent, hash: ${txHash}`)
await waitForTransactionReceipt(publicClient, { hash: txHash })
console.log('Score submitted successfully!')
} catch (e: any) {
console.error(`Failed to submit score: ${e.message}`)
}
}
main().catch(console.error)
import 'dotenv/config'
import { SDK, SchemaDecodedItem } from '@somnia-chain/streams'
import { createPublicClient, http } from 'viem'
import { somniaTestnet } from '../lib/chain'
import { leaderboardSchema } from '../libL/schema' // Our new schema
// --- DApp Contract Setup ---
const DAPP_CONTRACT_ADDRESS = '0x...' // Your deployed GameLeaderboard contract address
// --- --- ---
function getEnv(key: string): string {
const value = process.env[key]
if (!value) throw new Error(`Missing environment variable: ${key}`)
return value
}
const publicClient = createPublicClient({
chain: somniaTestnet,
transport: http(getEnv('RPC_URL')),
})
// Helper to decode the leaderboard data
interface ScoreRecord {
timestamp: number
player: `0x${string}`
score: bigint
}
function decodeScoreRecord(row: SchemaDecodedItem[]): ScoreRecord {
const val = (field: any) => field?.value?.value ?? field?.value ?? ''
return {
timestamp: Number(val(row[0])),
player: val(row[1]) as `0x${string}`,
score: BigInt(val(row[2])),
}
}
async function main() {
// The aggregator only needs a public client
const sdk = new SDK({ public: publicClient })
const schemaId = await sdk.streams.computeSchemaId(leaderboardSchema)
if (!schemaId) throw new Error('Could not compute schemaId')
console.log('--- Global Leaderboard Aggregator ---')
console.log(`Reading all data from proxy: ${DAPP_CONTRACT_ADDRESS}\n`)
// 1. Make ONE call to get all data for the DApp
const data = await sdk.streams.getAllPublisherDataForSchema(
schemaId,
DAPP_CONTRACT_ADDRESS
)
if (!data || data.length === 0) {
console.log('No scores found.')
return
}
// 2. Decode and sort the records
const allScores = (data as SchemaDecodedItem[][]).map(decodeScoreRecord)
allScores.sort((a, b) => (b.score > a.score ? 1 : -1)) // Sort descending by score
// 3. Display the leaderboard
console.log(`Total scores found: ${allScores.length}\n`)
allScores.forEach((record, index) => {
console.log(
`#${index + 1}: Player ${record.player} - Score: ${record.score} (at ${new Date(record.timestamp).toISOString()})`
)
})
}
main().catch(console.error)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount)
external
returns (bool);
function allowance(address owner, address spender)
external
view
returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount)
external
returns (bool);
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "./IERC20.sol";
contract ERC20 is IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner, address indexed spender, uint256 value
);
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
string public name;
string public symbol;
uint8 public decimals;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
}
function transfer(address recipient, uint256 amount)
external
returns (bool)
{
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount)
external
returns (bool)
{
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
function _mint(address to, uint256 amount) internal {
balanceOf[to] += amount;
totalSupply += amount;
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal {
balanceOf[from] -= amount;
totalSupply -= amount;
emit Transfer(from, address(0), amount);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function burn(address from, uint256 amount) external {
_burn(from, amount);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@openzeppelin/[email protected]/token/ERC20/ERC20.sol";
import "@openzeppelin/[email protected]/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/[email protected]/access/Ownable.sol";
contract MyToken is ERC20, ERC20Burnable, Ownable {
constructor(address initialOwner)
ERC20("MyToken", "MTK")
Ownable()
{}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}curl -L "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/verification/config"curl -L \
--request POST \
--url "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/<CONTRACT_ADDRESS>/verification/via/flattened-code" \
--header "Content-Type: application/json" \
--data '{
"compiler_version": "v0.8.24+commit.e11b9ed9",
"license_type": "mit",
"source_code": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\ncontract A { }",
"is_optimization_enabled": true,
"optimization_runs": 200,
"contract_name": "A",
"evm_version": "paris",
"autodetect_constructor_args": true
}'curl -L \
--request POST \
--url "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/<CONTRACT_ADDRESS>/verification/via/standard-input" \
--header "Content-Type: multipart/form-data" \
--form "compiler_version=v0.8.24+commit.e11b9ed9" \
--form "contract_name=MyContract" \
--form "files[0]=@./standard-input.json" \
--form "autodetect_constructor_args=true" \
--form "license_type=mit"curl -L \
--request POST \
--url "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/<CONTRACT_ADDRESS>/verification/via/multi-part" \
--header "Content-Type: multipart/form-data" \
--form "compiler_version=v0.8.24+commit.e11b9ed9" \
--form "license_type=mit" \
--form "is_optimization_enabled=true" \
--form "optimization_runs=200" \
--form "evm_version=paris" \
--form "files[0]=@./contracts/MyContract.sol" \
--form "files[1]=@./contracts/Lib.sol"npm install --save-dev @nomicfoundation/hardhat-verifyimport { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-verify";
const config: HardhatUserConfig = {
solidity: "0.8.24",
networks: {
"somnia-mainnet": {
url: "<SOMNIA_RPC_URL>",
accounts: ["<PRIVATE_KEY>"]
}
},
etherscan: {
apiKey: {
// A real API key is not required for Blockscout verification
"somnia-mainnet": "abc"
},
customChains: [
{
network: "somnia-mainnet",
chainId: 5031,
urls: {
apiURL: "https://mainnet.somnia.w3us.site/api",
browserURL: "https://mainnet.somnia.w3us.site/"
}
}
]
},
sourcify: {
enabled: false
}
};
export default config;npx hardhat verify \
--network somnia-mainnet \
<DEPLOYED_CONTRACT_ADDRESS> \
"<CONSTRUCTOR_ARG_1>" "<CONSTRUCTOR_ARG_2>"forge verify-contract \
--rpc-url <SOMNIA_RPC_URL> \
<DEPLOYED_CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract \
--verifier blockscout \
--verifier-url https://mainnet.somnia.w3us.site/api/forge create \
--rpc-url <SOMNIA_RPC_URL> \
--private-key $PRIVATE_KEY \
src/MyContract.sol:MyContract \
--verify \
--verifier blockscout \
--verifier-url https://mainnet.somnia.w3us.site/api/
npx create-next-app@latest somnia-chat --ts --app --no-tailwind
cd somnia-chatnpm i @somnia-chain/streams viemNEXT_PUBLIC_PUBLISHER_ADDRESS=0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03
NEXT_PUBLIC_RPC_URL=https://dream-rpc.somnia.network// lib/schema.ts
export const tapSchema = 'uint64 timestamp, address player'// lib/serverClient.ts
import { createPublicClient, http } from 'viem'
import { somniaTestnet } from 'viem/chains'
export function getServerPublicClient() {
return createPublicClient({
chain: somniaTestnet,
transport: http(process.env.RPC_URL || 'https://dream-rpc.somnia.network'),
})
}// lib/clients.ts
'use client'
import { createPublicClient, http } from 'viem'
import { somniaTestnet } from 'viem/chains'
export function getPublicHttpClient() {
return createPublicClient({
chain: somniaTestnet,
transport: http(process.env.NEXT_PUBLIC_RPC_URL || 'https://dream-rpc.somnia.network'),
})
}const [address, setAddress] = useState('')
const [walletClient, setWalletClient] = useState<any>(null)
const [cooldownMs, setCooldownMs] = useState(0)
const [pending, setPending] = useState(false)
const [error, setError] = useState('')async function connectWallet() {
if (typeof window !== "undefined" && window.ethereum !== undefined)
try {
await window.ethereum.request({ method: "eth_requestAccounts" });
const walletClient = createWalletClient({
chain: somniaDream,
transport: custom(window.ethereum),
});
const [account] = await walletClient.getAddresses();
setWalletClient(walletClient)
setAddress(account)
} catch (e: any) {
setError(e?.message || String(e))
} setWalletClient(wallet)
}const sdk = new SDK({
public: getPublicHttpClient(),
wallet: walletClient,
})tapSchema = 'uint64 timestamp, address player'const schemaId = await sdk.streams.computeSchemaId(tapSchema)// Register schema
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
const isRegistered = await sdk.streams.isDataSchemaRegistered(schemaId)
if (!isRegistered) {
const ignoreAlreadyRegistered = true
const txHash = await sdk.streams.registerDataSchemas(
[{ schemaName: 'tap', schema: tapSchema, parentSchemaId: zeroBytes32 }],
ignoreAlreadyRegistered
)
if (!txHash) throw new Error('Failed to register schema')
await waitForTransactionReceipt(getPublicHttpClient(), { hash: txHash })
}const encoder = new SchemaEncoder(tapSchema)
const now = BigInt(Date.now())
const data = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'player', value: address, type: 'address' },
])const id = keccak256(toHex(`${address}-${Number(nonce)}`))await sdk.streams.set([{ id, schemaId, data }])setCooldownMs(1000)
setPending(false)async function sendTap() {
if (!walletClient || !address) return
setPending(true)
const sdk = new SDK({ public: getPublicHttpClient(), wallet: walletClient })
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
const encoder = new SchemaEncoder(tapSchema)
const now = BigInt(Date.now())
const data = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'player', value: address, type: 'address' },
])
const id = keccak256(toHex(`${address}-${Number(now)}`))
await sdk.streams.set([{ id, schemaId, data }])
setCooldownMs(1000)
setPending(false)
}'use client'
import { useState, useEffect, useRef } from 'react'
import { SDK, SchemaEncoder } from '@somnia-chain/streams'
import { getPublicHttpClient } from '@/lib/clients'
import { tapSchema } from '@/lib/schema'
import { keccak256, toHex, createWalletClient, custom } from 'viem'
import { somniaTestnet } from 'viem/chains'
export default function Page() {
const [address, setAddress] = useState('')
const [walletClient, setWalletClient] = useState<any>(null)
const [leaderboard, setLeaderboard] = useState<{ address: string; count: number }[]>([])
const [cooldownMs, setCooldownMs] = useState(0)
const [pending, setPending] = useState(false)
const [error, setError] = useState('')
const lastNonce = useRef<number>(0)
async function connectWallet() {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
const wallet = createWalletClient({
chain: somniaTestnet,
transport: custom(window.ethereum),
})
setAddress(accounts[0])
setWalletClient(wallet)
}
async function sendTap() {
if (!walletClient || !address) return
setPending(true)
const sdk = new SDK({ public: getPublicHttpClient(), wallet: walletClient })
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
const encoder = new SchemaEncoder(tapSchema)
const now = BigInt(Date.now())
const data = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'player', value: address, type: 'address' },
{ name: 'nonce', value: BigInt(lastNonce.current++), type: 'uint256' },
])
const id = keccak256(toHex(`${address}-${Number(now)}`))
await sdk.streams.set([{ id, schemaId, data }])
setCooldownMs(1000)
setPending(false)
}
return (
<main style={{ padding: 24 }}>
<h1>🚀 Somnia Tap Game</h1>
{!address ? (
<button onClick={connectWallet}>🦊 Connect MetaMask</button>
) : (
<p>Connected: {address.slice(0, 6)}...{address.slice(-4)}</p>
)}
<button onClick={sendTap} disabled={pending || cooldownMs > 0 || !address}>
{pending ? 'Sending...' : '🖱️ Tap'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
<Leaderboard leaderboard={leaderboard} />
</main>
)
}
function Leaderboard({ leaderboard }: { leaderboard: { address: string; count: number }[] }) {
if (!leaderboard.length) return <p>No taps yet</p>
return (
<ol>
{leaderboard.map((p, i) => (
<li key={p.address}>
#{i + 1} {p.address} — {p.count} taps
</li>
))}
</ol>
)
}lib/store.ts
import { SDK } from '@somnia-chain/streams'
import { getServerPublicClient } from './serverClient'
import { tapSchema } from './schema'
const publisher =
process.env.NEXT_PUBLIC_PUBLISHER_ADDRESS ||
'0x0000000000000000000000000000000000000000'
const val = (f: any) => f?.value?.value ?? f?.value
export async function getLeaderboard() {
const sdk = new SDK({ public: getServerPublicClient() })
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
const rows = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)
if (!Array.isArray(rows)) return []
const counts = new Map<string, number>()
for (const row of rows) {
const player = String(val(row[1]) ?? '').toLowerCase()
if (!player.startsWith('0x')) continue
counts.set(player, (counts.get(player) || 0) + 1)
}
return Array.from(counts.entries())
.map(([address, count]) => ({ address, count }))
.sort((a, b) => b.count - a.count)
}import { NextResponse } from 'next/server'
import { getLeaderboard } from '@/lib/store'
export async function GET() {
const leaderboard = await getLeaderboard()
return NextResponse.json({ leaderboard })
}npm run devtry {
await treasury.withdraw(1000);
} catch (error: any) {
console.log('Revert reason:', error.reason || error.message);
console.log('Full error data:', error.data || error.error?.data);
}await expect(treasury.connect(user).withdraw(1000))
.to.be.revertedWith('Insufficient funds');const iface = new ethers.utils.Interface(contractABI);
try {
await treasury.connect(attacker).withdraw(9999);
} catch (error: any) {
const data = error.data || error.error?.data;
if (data) {
const decoded = iface.parseError(data);
console.log(`Custom Error: ${decoded.name}`);
console.log('Arguments:', decoded.args);
}
}if (error.data?.startsWith('0x4e487b71')) {
const code = parseInt(error.data.slice(10), 16);
console.log('Panic Code:', `0x${code.toString(16)}`);
}npx hardhat test --traceCALL treasury.withdraw
└─ CALL token.transfer -> reverted with reason: 'Insufficient balance'import axios from 'axios';
const abiURL = `https://explorer.somnia.network/api?module=contract&action=getabi&address=${address}`;
const { data } = await axios.get(abiURL);
const iface = new ethers.utils.Interface(JSON.parse(data.result));error Unauthorized(address caller);
function mint(address to, uint amount) external {
if (msg.sender != owner) revert Unauthorized(msg.sender);
_mint(to, amount);
}try {
await treasury.connect(randomUser).mint(addr, 100);
} catch (e: any) {
const iface = new ethers.utils.Interface(['error Unauthorized(address caller)']);
const decoded = iface.parseError(e.data);
console.log('Unauthorized address:', decoded.args[0]);
}forge test -vvvvconst implAddr = await provider.getStorageAt(proxyAddress, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc');
const iface = new ethers.utils.Interface(implementationABI);npx hardhat node --fork https://api.infra.mainnet.somnia.networknetworks: {
hardhat: {
forking: {
url: process.env.SOMNIA_RPC_TESTNET,
blockNumber: 123456, //example
}
}
}try {
const result = await treasury.callStatic.withdraw(1000);
console.log('Call successful:', result);
} catch (error: any) {
console.log('Simulation failed with reason:', error.reason);
}const tx = {
to: contract.address,
data: contract.interface.encodeFunctionData('stake', [amount])
};
const result = await provider.call(tx);
console.log('Returned data:', result);await network.provider.request({
method: 'hardhat_impersonateAccount',
params: ['0xAdminAddress']
});
const admin = await ethers.getSigner('0xAdminAddress');
await treasury.connect(admin).setFee(5);await network.provider.request({
method: 'hardhat_stopImpersonatingAccount',
params: ['0xAdminAddress']
});const snapshot = await network.provider.send('evm_snapshot', []);
await treasury.mint(100);
await network.provider.send('evm_revert', [snapshot]);const tx = await provider.getTransaction('0x123...');
await provider.call({ to: tx.to!, data: tx.data });anvil --fork-url https://dream-rpc.somnia.network --fork-block-number 3456789
forge test -vvvvvm.startPrank(admin);
contract.withdraw(1000);
vm.stopPrank();const gas = await contract.estimateGas.executeTrade(orderId);
console.log('Estimated gas:', gas.toString());// Manual key rotation implementation
// Note: RPC providers typically require manual key generation through their dashboard
// This implementation helps manage the rotation process once you have new keys
const updateEnvironmentVariable = async (key, value) => {
// Update .env file or environment configuration
const fs = require('fs').promises;
const envPath = '.env';
try {
let envContent = await fs.readFile(envPath, 'utf8');
const regex = new RegExp(`^${key}=.*$`, 'm');
if (regex.test(envContent)) {
envContent = envContent.replace(regex, `${key}=${value}`);
} else {
envContent += `\n${key}=${value}`;
}
await fs.writeFile(envPath, envContent);
console.log(`Updated ${key} in environment file`);
} catch (error) {
console.error('Failed to update environment variable:', error);
throw error;
}
};
// Manual key rotation helper
const rotateApiKey = async (newKey) => {
try {
// Validate the new key format
if (!newKey || typeof newKey !== 'string') {
throw new Error('Invalid API key provided');
}
// Store old key for reference
const oldKey = process.env.SOMNIA_TESTNET_RPC_URL;
console.log('Rotating API key...');
// Update environment variable
await updateEnvironmentVariable('SOMNIA_TESTNET_RPC_URL', newKey);
console.log('API key rotated successfully');
console.log('Please manually revoke the old key in your RPC provider dashboard');
console.log('Old key (first 10 chars):', oldKey?.substring(0, 10) + '...');
} catch (error) {
console.error('Key rotation failed:', error);
}
};
// Key rotation reminder system
const setupRotationReminder = () => {
const NINETY_DAYS = 90 * 24 * 60 * 60 * 1000;
setInterval(() => {
console.log('\n🔑 SECURITY REMINDER: Consider rotating your RPC API keys');
console.log('1. Generate new key in your RPC provider dashboard');
console.log('2. Call rotateApiKey(newKey) with the new key');
console.log('3. Manually revoke old key in provider dashboard\n');
}, NINETY_DAYS);
};
// Usage example:
// rotateApiKey('https://rpc.ankr.com/somnia_testnet/your-new-private-key');
// setupRotationReminder();// Secure logging implementation
const winston = require('winston');
// Create logger with security considerations
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
// Custom format to redact sensitive information
winston.format.printf(({ timestamp, level, message, ...meta }) => {
// Redact sensitive data
const sanitized = JSON.stringify(meta).replace(
/(private_key|api_key|secret)":\s*"[^"]+"/gi,
'$1": "[REDACTED]"'
);
return `${timestamp} [${level}]: ${message} ${sanitized}`;
})
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Error handling for RPC calls
const safeRpcCall = async (provider, method, params) => {
try {
const result = await provider.send(method, params);
logger.info('RPC call successful', { method, success: true });
return result;
} catch (error) {
// Log error without exposing sensitive information
logger.error('RPC call failed', {
method,
error: error.message,
code: error.code
});
throw new Error(`RPC call failed: ${error.message}`);
}
};// Do not call your api Provider directly in your script with the api-keys!
// Setup provider AnkrProvider
const provider = new AnkrProvider('https://rpc.ankr.com/somnia_testnet/your-private-key');// Create a .env file
SOMNIA_ANKR_RPC_URL=https://rpc.ankr.com/somnia_testnet/your-private-key
// Use environment variables
// Setup provider AnkrProvider
const provider = new AnkrProvider(process.env.SOMNIA_ANKR_RPC_URL);// Example configuration for private endpoint
const config = {
testnet: {
url: process.env.SOMNIA_TESTNET_RPC_URL,
accounts: [process.env.TESTNET_PRIVATE_KEY]
}
};# .env file
SOMNIA_TESTNET_RPC_URL=https://rpc.ankr.com/somnia_testnet/your-private-key
TESTNET_PRIVATE_KEY=your_private_key_here
NODE_ENV=development# .gitignore
.env
.env.local
.env.*.local
node_modules/
dist/# Project structure
├── .env.example # Template file (safe to commit)
├── .env.development # Development secrets
├── .env.test # Test environment
├── .env.staging # Staging environment
└── .env.production # Production secrets (never commit)// config/environment.js
const dotenv = require('dotenv');
const path = require('path');
const environment = process.env.NODE_ENV || 'development';
const envFile = `.env.${environment}`;
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
module.exports = {
rpcUrl: process.env.SOMNIA_RPC_URL,
privateKey: process.env.PRIVATE_KEY,
environment
};// utils/blockchain.js
const { ethers } = require('ethers');
const config = require('../config/environment');
class BlockchainService {
constructor() {
this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
this.wallet = new ethers.Wallet(config.privateKey, this.provider);
}
async getBalance(address) {
return await this.provider.getBalance(address);
}
async sendTransaction(to, value) {
const tx = {
to,
value: ethers.parseEther(value.toString())
};
return await this.wallet.sendTransaction(tx);
}
}
module.exports = BlockchainService;// test-env.js - For application projects only
require('dotenv').config();
const testEnvironmentVariables = () => {
const requiredVars = [
'SOMNIA_TESTNET_RPC_URL',
'TESTNET_PRIVATE_KEY'
];
const missing = requiredVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
console.error('Missing required environment variables:', missing);
process.exit(1);
}
console.log('All required environment variables are loaded');
};
testEnvironmentVariables();# 1. Initialize project
npm init -y
npm install ethers dotenv
npm install -D nodemon
# 2. Create environment template
echo "SOMNIA_RPC_URL=https://rpc.ankr.com/somnia_testnet/your-key-here" > .env.example
echo "PRIVATE_KEY=your-private-key-here" >> .env.example
echo "CONTRACT_ADDRESS=0x..." >> .env.example
# 3. Add to .gitignore
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore// contracts/SomniaContract.js
const { ethers } = require('ethers');
const config = require('../config/environment');
class SomniaContract {
constructor(contractAddress, abi) {
this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
this.wallet = new ethers.Wallet(config.privateKey, this.provider);
this.contract = new ethers.Contract(contractAddress, abi, this.wallet);
}
async safeCall(methodName, ...args) {
try {
// Estimate gas first
const gasEstimate = await this.contract[methodName].estimateGas(...args);
// Add 20% buffer
const gasLimit = gasEstimate * 120n / 100n;
const tx = await this.contract[methodName](...args, { gasLimit });
console.log(`Transaction sent: ${tx.hash}`);
const receipt = await tx.wait();
console.log(`Transaction confirmed: ${receipt.transactionHash}`);
return receipt;
} catch (error) {
console.error('Transaction failed:', error.message);
throw error;
}
}
}
module.exports = SomniaContract;# Example: Configure IP allowlist in your provider dashboard
# Allowed IPs: 203.0.113.1, 203.0.113.2
# This ensures only requests from your servers can use the key// AWS Secrets Manager example
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
const getRpcKey = async () => {
const secret = await secretsManager.getSecretValue({
SecretId: 'somnia-rpc-key'
}).promise();
return JSON.parse(secret.SecretString).rpcUrl;
};// Example: Secure key generation with ethers.js
const { Wallet } = require('ethers');
const { randomBytes } = require('crypto');
// Generate cryptographically secure random wallet
const generateSecureWallet = () => {
const randomWallet = Wallet.createRandom();
return {
address: randomWallet.address,
privateKey: randomWallet.privateKey,
mnemonic: randomWallet.mnemonic.phrase
};
};// Example: Role-based access pattern
class SecureWalletManager {
constructor() {
this.roles = new Map();
this.permissions = {
'admin': ['deploy', 'transfer', 'read'],
'developer': ['deploy', 'read'],
'viewer': ['read']
};
}
assignRole(address, role) {
this.roles.set(address, role);
}
canExecute(address, action) {
const role = this.roles.get(address);
return this.permissions[role]?.includes(action) || false;
}
}// Implement retry logic with exponential backoff
const retryRpcCall = async (provider, method, params, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await safeRpcCall(provider, method, params);
} catch (error) {
if (attempt === maxRetries) {
logger.error('Max retries exceeded', { method, attempts: attempt });
throw error;
}
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
logger.warn('Retrying RPC call', { method, attempt, delay });
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};import { createContext, useContext, useState } from "react";
import {
defineChain,
createPublicClient,
createWalletClient,
http,
custom,
parseEther,
} from "viem";
import { ABI } from "../../abi"; // Adjust the path as necessary
// Define Somnia Chain
const SOMNIA = defineChain({
id: 50312,
name: "Somnia Testnet",
nativeCurrency: {
decimals: 18,
name: "Ether",
symbol: "STT",
},
rpcUrls: {
default: {
http: ["https://dream-rpc.somnia.network"],
},
},
blockExplorers: {
default: { name: "Explorer", url: "https://somnia-devnet.socialscan.io" },
},
});
// Create a public client for read operations
const publicClient = createPublicClient({
chain: SOMNIA,
transport: http(),
});
const WalletContext = createContext();
export function WalletProvider({ children }) {
// ---------- STATE ------------
const [connected, setConnected] = useState(false);
const [address, setAddress] = useState("");
const [client, setClient] = useState(null);
// Fetch Total Proposals
async function fetchTotalProposals() {
try {
const result = await publicClient.readContract({
address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4",
abi: ABI,
functionName: "totalProposals",
});
return result; // Returns a BigInt
} catch (error) {
console.error("Error fetching totalProposals:", error);
throw error;
}
}
// Fetch Proposal Details
async function fetchProposal(proposalId) {
try {
const result = await publicClient.readContract({
address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4",
abi: ABI,
functionName: "proposals",
args: [parseInt(proposalId)],
});
console.log(result);
return result; // Returns the Proposal struct
} catch (error) {
console.error("Error fetching proposal:", error);
throw error;
}
}
// Provider's value
return (
<WalletContext.Provider
value={{
connected,
address,
client,
connectToMetaMask,
disconnectWallet,
fetchTotalProposals,
fetchProposal,
}}
>
{children}
</WalletContext.Provider>
);
}
// Custom hook to consume context
export function useWallet() {
return useContext(WalletContext);
}import { useState, useEffect } from "react";
import ConnectButton from "../components/connectbutton";
import { useWallet } from "../contexts/walletcontext";
export default function Home() {
const { fetchTotalProposals } = useWallet();
const [totalProposals, setTotalProposals] = useState(null);
useEffect(() => {
async function loadData() {
try {
const count = await fetchTotalProposals();
setTotalProposals(count);
} catch (error) {
console.error("Failed to fetch total proposals:", error);
}
}
loadData();
}, [fetchTotalProposals]);
return (
<div
className={`${geistSans.variable} ${geistMono.variable}
grid grid-rows-[20px_1fr_20px] items-center justify-items-center
min-h-screen p-8 pb-20 gap-16 sm:p-20
font-[family-name:var(--font-geist-sans)]`}
>
{/* The NavBar is already rendered in _app.js */}
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<h1 className="text-3xl font-bold">Welcome to MyDAO</h1>
{totalProposals !== null ? (
<p className="text-lg">
Total proposals created: {totalProposals.toString()}
</p>
) : (
<p>Loading total proposals...</p>
)}
<ConnectButton />
</main>
</div>
);
}import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { useWallet } from "../contexts/walletcontext";
import { Button, Card, Label, TextInput } from "flowbite-react"; // Optional Flowbite imports
export default function FetchProposalPage() {
const [proposalId, setProposalId] = useState("");
const [proposalData, setProposalData] = useState(null);
const [error, setError] = useState("");
const { connected, fetchProposal, voteOnProposal, executeProposal } = useWallet();
const handleSubmit = async (e) => {
e.preventDefault();
setError(""); // Clear previous errors
if (!connected) {
alert("You must connect your wallet first!");
return;
}
if (!proposalId.trim()) {
setError("Please enter a proposal ID.");
return;
}
try {
// Fetch the proposal from the contract
const result = await fetchProposal(proposalId);
console.log("Fetched Proposal:", result);
setProposalData(result);
} catch (err) {
console.error("Error fetching proposal:", err);
setError("Failed to fetch proposal. Check console for details.");
}
};
useEffect(() => {
if (proposalData !== null) {
console.log("Updated Proposal Data:", proposalData);
}
}, [proposalData]);
return (
<div className="max-w-2xl mx-auto mt-20 p-4">
<h1 className="text-2xl font-bold mb-4">Fetch a Proposal</h1>
{/* Form to input Proposal ID */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="proposal-id" value="Proposal ID" />
<TextInput
id="proposal-id"
type="number"
placeholder="Enter proposal ID"
value={proposalId}
onChange={(e) => setProposalId(e.target.value)}
required
/>
</div>
<Button type="submit" color="blue">
Fetch
</Button>
</form>
{/* Display Errors */}
{error && <div className="mt-4 text-red-600">{error}</div>}
{/* Display Proposal Details */}
{proposalData && (
<Card className="mt-8">
<h2 className="text-xl font-bold mb-2">Proposal #{proposalId}</h2>
<ul className="list-disc list-inside space-y-1">
<li>
<strong>Description:</strong> {proposalData[0]}
</li>
<li>
<strong>Deadline:</strong> {new Date(proposalData[1] * 1000).toLocaleString()}
</li>
<li>
<strong>Yes Votes:</strong> {proposalData[2].toString()}
</li>
<li>
<strong>No Votes:</strong> {proposalData[3].toString()}
</li>
<li>
<strong>Executed:</strong> {proposalData[4] ? "Yes" : "No"}
</li>
<li>
<strong>Proposer:</strong> {proposalData[5]}
</li>
</ul>
</div>
</Card>
)}
</div>
);
}const [loading, setLoading] = useState(false);
// In handleSubmit
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
setLoading(true);
// ... rest of the code
setLoading(false);
};
// In the button
<Button type="submit" color="blue" disabled={loading}>
{loading ? "Fetching..." : "Fetch"}
</Button>npm run dev// const { ethers } = require("ethers");
import { ethers } from "ethers";
// Configuration
const wsUrl = "wss://dream-rpc.somnia.network/ws";
const contractAddress = "0xADA7b2953E7d670092644d37b6a39BAE3237beD7"; // Replace with your contract address
// Contract ABI
const abi = [
{
anonymous: false,
inputs: [
{ indexed: true, internalType: "string", name: "oldGreeting"
{ indexed: true, internalType: "string", name: "newGreeting"
],
name: "GreetingSet",
type: "event",
},
{
inputs: [],
name: "getGreeting",
outputs: [{ internalType: "string", name: "", type:
stateMutability: "view",
type: "function",
},
];
async function listen() {
// Create WebSocket provider and contract
const provider = new ethers.WebSocketProvider(wsUrl);
await provider._waitUntilReady();
const contract = new ethers.Contract(contractAddress, abi, provider);
console.log("Listening for events...\n");
// Event filter
const filter = {
address: contractAddress,
topics: [ethers.id("GreetingSet(string,string)")],
};
// Listen for events
provider.on(filter, async (log) => {
try {
const greeting = await contract.getGreeting();
console.log(`New greeting: "${greeting}"`);
} catch (error) {
console.error("Error:", error.message);
}
});
// Keep connection alive
setInterval(async () => {
try {
await provider.getBlockNumber();
} catch (error) {
console.error("Connection error");
}
}, 30000);
// Handle shutdown
process.on("SIGINT", () => {
provider.destroy();
process.exit(0);
});
}
// Start listening
listen().catch(console.error);



Client Server
| |
|----Connection Request-->|
|<---Connection Accept----|
| |
|<===Open Connection====> |
| |
|----Send Message-------->|
|<---Receive Message------|
|<---Push Notification----|
|----Send Message-------->|
| |
|<===Open Connection====> |
| |
|----Close Connection---->|// Inefficient: Constantly asking "Any updates?"
setInterval(async () => {
const response = await fetch('https://api.example.com/events');
const data = await response.json();
if (data.hasNewEvents) {
console.log('New event:', data.events);
}
}, 5000); // Check every 5 seconds// Efficient: Server pushes updates immediately
const ws = new WebSocket('wss://api.example.com/events');
ws.on('message', (data) => {
console.log('New event:', data); // Instant notification
});event MessageSent(string message); // Can read 'message' directly from logsevent MessageSent(string indexed message); // 'message' is hashed, cannot read directlyconst wsUrl = 'wss://api.infra.testnet.somnia.network/ws'; //change url for Mainnet
const provider = new ethers.WebSocketProvider(wsUrl);
await provider._waitUntilReady();const contract = new ethers.Contract(contractAddress, abi, provider);const filter = {
address: contractAddress,
topics: [ethers.id("GreetingSet(string,string)")]
};provider.on(filter, async (log) => {
// Handle the event
const greeting = await contract.getGreeting();
console.log(`New greeting: "${greeting}"`);
});setInterval(async () => {
await provider.getBlockNumber();
}, 30000);const abi = [
// Your event definition
{
"anonymous": false,
"inputs": [
// Your event parameters
],
"name": "YourEventName",
"type": "event"
},
// Any read functions you need
{
"inputs": [],
"name": "yourReadFunction",
"outputs": [/* outputs */],
"stateMutability": "view",
"type": "function"
}
];const filter = {
address: contractAddress,
topics: [ethers.id("YourEventName(type1,type2)")]
};provider.on(filter, async (log) => {
// For non-indexed parameters, you can parse the log
const parsedLog = contract.interface.parseLog(log);
// For indexed strings, query the contract state
const currentState = await contract.yourReadFunction();
// Process your data
console.log('Event detected:', currentState);
});// Listen for Event1
provider.on({
address: contractAddress,
topics: [ethers.id("Event1(...)")]
}, handleEvent1);
// Listen for Event2
provider.on({
address: contractAddress,
topics: [ethers.id("Event2(...)")]
}, handleEvent2);async function connectWithRetry() {
let retries = 0;
while (retries < 5) {
try {
await listen();
break;
} catch (error) {
console.log(`Retry ${++retries}/5...`);
await new Promise(r => setTimeout(r, 5000));
}
}
}// Get last 100 blocks of events
const currentBlock = await provider.getBlockNumber();
const events = await contract.queryFilter('YourEventName', currentBlock - 100, currentBlock);
events.forEach(event => {
console.log('Historical event:', event);
});node websocket-listener.js"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>
)
}npm i @somnia-chain/streams viem// 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)// lib/schema.ts
export const helloWorldSchema = 'uint64 timestamp, string message'
export const schemaId = '0xabc123...' // Example Schema ID
export const publisher = '0xF9D3...E5aC' // Example Publisher Addressconst computedId = await sdk.streams.computeSchemaId(helloWorldSchema)
console.log('Computed Schema ID:', computedId)// 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
}export async function getMessageById(messageKey: `0x${string}`) {
const msg = await sdk.streams.getByKey(schemaId, publisher, messageKey)
console.log('Message by key:', msg)
return msg
}export async function getMessageAtIndex(index: bigint) {
const record = await sdk.streams.getAtIndex(schemaId, publisher, index)
console.log(`Record at index ${index}:`, record)
return record
}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
}export async function getAllPublisherData() {
const allData = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)
console.log('All publisher data:', allData)
return allData
}export async function getTotalEntries() {
const total = await sdk.streams.totalPublisherDataForSchema(schemaId, publisher)
console.log(`Total entries: ${total}`)
return Number(total)
} 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
}
}const schemaInfo = await sdk.streams.getSchemaFromSchemaId(schemaId)
console.log('Schema Info:', schemaInfo){
baseSchema: 'uint64 timestamp, string message',
finalSchema: 'uint64 timestamp, string message',
schemaId: '0xabc123...'
}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 -psomnia-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.jsonimport { 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)export const helloWorldSchema = "uint64 timestamp, string message"
export const schemaId = "0xabc123..." // replace with actual schemaId
export const publisher = "0xF9D3...E5aC" // replace with actual publisherconst computed = await sdk.streams.computeSchemaId(helloWorldSchema)
console.log("Schema ID:", computed)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)
}import { NextResponse } from "next/server"
import { getLatestMessage } from "@/lib/read"
export async function GET() {
const data = await getLatestMessage()
return NextResponse.json({ data })
}"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>
)
}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>
)
}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>
)
}npm run dev// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {VRFConsumerBaseV2Plus} from "@chainlink/[email protected]/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/[email protected]/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
import {VRFV2PlusWrapperConsumerBase} from "@chainlink/[email protected]/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol";
import {ConfirmedOwner} from "@chainlink/[email protected]/src/v0.8/shared/access/ConfirmedOwner.sol";
contract RandomNumberConsumer is VRFV2PlusWrapperConsumerBase, ConfirmedOwner {
uint256 public latestRequestId;
uint256[] public latestRandomWord;
bool public fulfilled;
uint32 public constant CALLBACK_GAS_LIMIT = 2_100_000;
uint16 public constant REQUEST_CONFIRMATIONS = 3;
uint32 public constant NUM_WORDS = 3;
event RandomNumberRequested(uint256 indexed requestId, address indexed requester, uint256 paid);
event RandomNumberFulfilled(uint256 indexed requestId, uint256[] randomWord);
error InsufficientPayment(uint256 required, uint256 sent);
error RequestAlreadyPending();
constructor(address wrapper)
ConfirmedOwner(msg.sender)
VRFV2PlusWrapperConsumerBase(wrapper)
{}
function requestRandomNumber() external payable onlyOwner {
// Check if there's already a pending request
if (latestRequestId != 0 && !fulfilled) {
revert RequestAlreadyPending();
}
// Calculate the required payment
uint256 requestPrice = getRequestPrice();
if (msg.value < requestPrice) {
revert InsufficientPayment(requestPrice, msg.value);
}
// Prepare the extra arguments for native payment
VRFV2PlusClient.ExtraArgsV1 memory extraArgs = VRFV2PlusClient.ExtraArgsV1({
nativePayment: true
});
bytes memory args = VRFV2PlusClient._argsToBytes(extraArgs);
// Request randomness
(uint256 requestId, uint256 paid) = requestRandomnessPayInNative(
CALLBACK_GAS_LIMIT,
REQUEST_CONFIRMATIONS,
NUM_WORDS,
args
);
latestRequestId = requestId;
fulfilled = false;
emit RandomNumberRequested(requestId, msg.sender, paid);
// Refund excess payment
if (msg.value > paid) {
(bool success, ) = msg.sender.call{value: msg.value - paid}("");
require(success, "Refund failed");
}
}
// This will be called by the VRF Wrapper
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
require(randomWords.length > 0, "No random word returned");
require(requestId == latestRequestId, "Unexpected request ID");
latestRandomWord = randomWords;
fulfilled = true;
emit RandomNumberFulfilled(requestId, randomWords);
}
function getRequestStatus() external view returns (
uint256 requestId,
bool isPending,
bool isFulfilled
) {
return (
latestRequestId,
latestRequestId != 0 && !fulfilled,
fulfilled
);
}
function getLatestRandomWord() external view returns (uint256[] memory) {
require(fulfilled, "No fulfilled request yet");
return latestRandomWord;
}
/**
* @notice Get the current price for a VRF request in native tokens
* @return The price in wei for requesting random numbers
*/
function getRequestPrice() public view returns (uint256) {
return i_vrfV2PlusWrapper.calculateRequestPriceNative(CALLBACK_GAS_LIMIT, NUM_WORDS);
}
/**
* @notice Withdraw any excess native tokens from the contract
* @dev Only callable by owner, useful for recovering overpayments
*/
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No balance to withdraw");
(bool success, ) = owner().call{value: balance}("");
require(success, "Withdrawal failed");
}
// Allow contract to receive STT for native payment
receive() external payable {}
}contract RandomNumberConsumer
is VRFV2PlusWrapperConsumerBase, ConfirmedOwneruint256 public latestRequestId;
uint256[] public latestRandomWord;
bool public fulfilled;uint32 public constant CALLBACK_GAS_LIMIT = 2_100_000;
uint16 public constant REQUEST_CONFIRMATIONS = 3;
uint32 public constant NUM_WORDS = 3;event RandomNumberRequested(uint256 indexed requestId, address indexed requester, uint256 paid);
event RandomNumberFulfilled(uint256 indexed requestId, uint256[] randomWord);error InsufficientPayment(uint256 required, uint256 sent);
error RequestAlreadyPending();constructor(address wrapper) ConfirmedOwner(msg.sender) VRFV2PlusWrapperConsumerBase(wrapper) {}function requestRandomNumber() external payable onlyOwner {
// 1) block overlapping requests
if (latestRequestId != 0 && !fulfilled) revert RequestAlreadyPending();
// 2) compute required fee and validate payment
uint256 requestPrice = getRequestPrice();
if (msg.value < requestPrice) revert InsufficientPayment(requestPrice, msg.value);
// 3) signal native payment to the wrapper
bytes memory args = VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({ nativePayment: true })
);
// 4) submit request (uses native STT)
(uint256 requestId, uint256 paid) = requestRandomnessPayInNative(
CALLBACK_GAS_LIMIT,
REQUEST_CONFIRMATIONS,
NUM_WORDS,
args
);
latestRequestId = requestId;
fulfilled = false;
emit RandomNumberRequested(requestId, msg.sender, paid);
// 5) refund any excess back to caller
if (msg.value > paid) {
(bool ok, ) = msg.sender.call{ value: msg.value - paid }("");
require(ok, "Refund failed");
}
}function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
require(randomWords.length > 0, "No random word returned");
require(requestId == latestRequestId, "Unexpected request ID");
latestRandomWord = randomWords; // stores 3 words
fulfilled = true;
emit RandomNumberFulfilled(requestId, randomWords);
}function getRequestStatus()
external
view
returns (uint256 requestId, bool isPending, bool isFulfilled)
{
return (latestRequestId, latestRequestId != 0 && !fulfilled, fulfilled);
}function getLatestRandomWord() external view returns (uint256[] memory) {
require(fulfilled, "No fulfilled request yet");
return latestRandomWord;
}function getRequestPrice() public view returns (uint256) {
return i_vrfV2PlusWrapper.calculateRequestPriceNative(CALLBACK_GAS_LIMIT, NUM_WORDS);
}// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.4.0
pragma solidity ^0.8.27;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract NFTTest is ERC721, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
constructor(address initialOwner)
ERC721("NFTTest", "NFTT")
Ownable(initialOwner)
{}
function _baseURI() internal pure override returns (string memory) {
return "https://ipfs.io"; // only affects relative paths
}
function safeMint(address to, string memory uri)
public
onlyOwner
returns (uint256)
{
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri); // store full URI (e.g., ipfs://CID/0.json)
return tokenId;
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
contract OnchainMeta is ERC721, Ownable {
using Strings for uint256;
uint256 private _id;
constructor(address owner_) ERC721("Onchain", "ONC") Ownable(owner_) {}
function mint(address to) external onlyOwner returns (uint256) {
uint256 tokenId = _id++;
_safeMint(to, tokenId);
return tokenId;
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);
// Example: trivial, static image via SVG data URI
string memory image = string(
abi.encodePacked(
"data:image/svg+xml;base64,",
Base64.encode(
bytes(
string(
abi.encodePacked(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>",
"<rect width='512' height='512' fill='black'/>",
"<text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='48'>#",
tokenId.toString(),
"</text></svg>"
)
)
)
)
)
);
bytes memory json = abi.encodePacked(
'{"name":"Onchain #', tokenId.toString(),
'","description":"Fully on-chain metadata","image":"', image, '"}'
);
return string(abi.encodePacked(
"data:application/json;base64,",
Base64.encode(json)
));
}
}npm i sharp fast-glob// scripts/resize.js
import fg from "fast-glob";
import sharp from "sharp";
import { mkdirSync } from "fs";
import path from "path";
const INPUT = "assets/raw-images"; // put your original images here
const OUTPUT = "assets/images";
mkdirSync(OUTPUT, { recursive: true });
const files = (await fg(`${INPUT}/*.{png,jpg,jpeg,webp}`)).sort();
for (let i = 0; i < files.length; i++) {
const out = path.join(OUTPUT, `${i}.png`);
await sharp(files[i]).resize(1024, 1024, { fit: "cover" }).toFile(out);
}
console.log("Normalized images written to", OUTPUT);project/
assets/
images/
0.png
1.png
2.png
...
metadata/
0.json
1.json
2.json
...
scripts/
resize.js
pinata.ts (Pinata SDK init)
upload-images.ts (pin images folder → IMAGES_CID)
make-metadata.ts (generate JSON → uses IMAGES_CID)
upload-metadata.ts (pin metadata folder → METADATA_CID)npm i pinata dotenv// pinata.ts
import 'dotenv/config'
import { PinataSDK } from 'pinata'
export const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!, // from Pinata “API Keys”
pinataGateway: process.env.PINATA_GATEWAY!, // e.g. myxyz.mypinata.cloud
})PINATA_JWT=eyJhbGciOi... # your Pinata JWT (keep secret)
PINATA_GATEWAY=myxyz.mypinata.cloud
IMAGES_CID= # leave blank until you upload images// scripts/upload-images.ts
import 'dotenv/config'
import { pinata } from './pinata'
import fs from 'node:fs/promises'
import path from 'node:path'
import { File } from 'node:buffer'
const DIR = 'assets/images'
async function main() {
const names = (await fs.readdir(DIR))
.filter(n => n.match(/\.(png|jpg|jpeg|webp)$/i))
.sort((a, b) => Number(a.split('.')[0]) - Number(b.split('.')[0]))
const files: File[] = []
for (const name of names) {
const bytes = await fs.readFile(path.join(DIR, name))
files.push(new File([bytes], name)) // uploaded as a single folder
}
const res = await pinata.upload.public.fileArray(files)
// res.cid is the directory root CID
console.log('Images CID:', res.cid)
}
main().catch(console.error)node dist/scripts/upload-images.js
# => Images CID: bafybe...{
"name": "NFTTest #0",
"description": "A clean ERC-721 on Somnia with per-token metadata.",
"image": "ipfs://IMAGES_CID/0.png",
"external_url": "https://your-site.example",
"attributes": [
{ "trait_type": "Edition", "value": 0 }
]
}
npm i fs-extra fast-glob/ scripts/make-metadata.js
import fg from "fast-glob";
import { writeJSON, mkdirs } from "fs-extra";
import path from "path";
const IMAGES_CID = process.env.IMAGES_CID || "bafy..."; // paste from Step 2
const OUT_DIR = "assets/metadata";
sync function main() {
await mkdirs(OUT_DIR)
const imgs = (await fg('assets/images/*.{png,jpg,jpeg,webp}')).sort((a, b) =>
Number(path.basename(a).split('.')[0]) - Number(path.basename(b).split('.')[0])
)
for (const p of imgs) {
const id = Number(path.basename(p).split('.')[0])
const json = {
name: `NFTTest #${id}`,
description: 'ERC-721 on Somnia with Pinata-hosted IPFS metadata.',
image: `ipfs://${IMAGES_CID}/${id}.png`,
attributes: [{ trait_type: 'Edition', value: id }]
}
await writeJSON(path.join(OUT_DIR, `${id}.json`), json, { spaces: 2 })
}
console.log('Metadata written to', OUT_DIR)
}
main().catch(console.error)
node dist/scripts/make-metadata.js// scripts/upload-metadata.ts
import { pinata } from './pinata'
import fs from 'node:fs/promises'
import path from 'node:path'
import { File } from 'node:buffer'
const DIR = 'assets/metadata'
async function main() {
const names = (await fs.readdir(DIR))
.filter(n => n.endsWith('.json'))
.sort((a, b) => Number(a.split('.')[0]) - Number(b.split('.')[0]))
const files: File[] = []
for (const name of names) {
const bytes = await fs.readFile(path.join(DIR, name))
files.push(new File([bytes], name, { type: 'application/json' }))
}
const res = await pinata.upload.public.fileArray(files)
console.log('Metadata CID:', res.cid)
}
main().catch(console.error)node dist/scripts/upload-metadata.js
# => Metadata CID: bafybe...scripts/mint.ts (Hardhat)
import { ethers } from "hardhat";
const CONTRACT = "0xYourDeployedAddress";
const RECEIVER = "0xReceiver";
const METADATA_CID = "bafy...";
async function main() {
const nft = await ethers.getContractAt("NFTTest", CONTRACT);
for (let id = 0; id < 10; id++) {
const uri = `ipfs://${METADATA_CID}/${id}.json`;
const tx = await nft.safeMint(RECEIVER, uri);
await tx.wait();
console.log(`Minted #${id} → ${uri}`);
}
}
main().catch(console.error);
RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEYuint64 timestamp, bytes32 roomId, string content, string senderName, address senderuint64 timestamp, bytes32 roomId, string content, string senderName, address sendernpm i @somnia-chain/streams viem
npm i -D @types/node// 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'] },
},
})// 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')),
})// src/lib/chatSchema.ts
export const chatSchema =
'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'// 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)
})// 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)
})// 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))

npx create-next-app@latest somnia-subgraph-ui --typescript --tailwind --app
cd somnia-subgraph-uinpm install @apollo/client graphqlUser Interface (React Components)
↓
Apollo Client (GraphQL Client)
↓
GraphQL Queries
↓
Somnia Subgraph API
↓
Blockchain Dataimport { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
// The URI of your subgraph endpoint
uri: 'https://proxy.somnia.chain.love/subgraphs/name/somnia-testnet/SomFlip',
// Apollo's caching layer - stores query results
cache: new InMemoryCache(),
});
export default client;'use client'; // Next.js 13+ directive for client-side components
import { ApolloProvider } from '@apollo/client';
import client from '@/lib/apollo-client';
// This component wraps your app with Apollo's context provider
export default function ApolloWrapper({
children
}: {
children: React.ReactNode
}) {
return (
<ApolloProvider client={client}>
{children}
</ApolloProvider>
);
}import ApolloWrapper from '@/components/ApolloWrapper';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ApolloWrapper>
{children}
</ApolloWrapper>
</body>
</html>
);
}import { gql } from '@apollo/client';
// Query for paginated flip results
export const GET_FLIP_RESULTS = gql`
query GetFlipResults(
$first: Int!, # Number of results to fetch
$skip: Int!, # Number of results to skip (for pagination)
$orderBy: String!, # Field to sort by
$orderDirection: String! # 'asc' or 'desc'
) {
flipResults(
first: $first
skip: $skip
orderBy: $orderBy
orderDirection: $orderDirection
) {
id # Unique identifier
player # Wallet address of player
betAmount # Amount bet (in wei)
choice # Player's choice: HEADS or TAILS
result # Actual result: HEADS or TAILS
payout # Amount won (0 if lost)
blockNumber # Block when flip occurred
blockTimestamp # Unix timestamp
transactionHash # Transaction hash on blockchain
}
}
`;
// Query for recent flips (live feed)
export const GET_RECENT_FLIPS = gql`
query GetRecentFlips($first: Int!) {
flipResults(
first: $first
orderBy: blockTimestamp
orderDirection: desc # Most recent first
) {
id
player
betAmount
choice
result
payout
blockTimestamp
transactionHash
}
}
`;'use client';
import { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_FLIP_RESULTS } from '@/lib/queries';/ Shortens long blockchain addresses for display
// Example: "0x1234567890abcdef" becomes "0x1234...cdef"
const truncateHash = (hash: string) => {
return `${hash.slice(0, 6)}...${hash.slice(-4)}`;
};
// Converts wei (smallest unit) to ether (display unit)
// 1 ether = 1,000,000,000,000,000,000 wei (10^18)
const formatEther = (wei: string) => {
const ether = parseFloat(wei) / 1e18;
return ether.toFixed(4); // Show 4 decimal places
};
// Converts Unix timestamp to readable date
// Blockchain stores time as seconds since Jan 1, 1970
const formatTime = (timestamp: string) => {
const milliseconds = parseInt(timestamp) * 1000;
const date = new Date(milliseconds);
return date.toLocaleString();
};export default function AllFlips() {
// Track which page of results we're viewing
const [page, setPage] = useState(0);
const itemsPerPage = 30;const { loading, error, data } = useQuery(GET_FLIP_RESULTS, {
variables: {
first: itemsPerPage, // How many results to fetch
skip: page * itemsPerPage, // How many to skip
orderBy: 'blockTimestamp', // Sort by time
orderDirection: 'desc', // Newest first
},
});// Show loading spinner while fetching
if (loading) {
return <div className="text-center py-8 text-gray-500">Loading...</div>;
}
// Show error message if query failed
if (error) {
return (
<div className="text-center py-8 text-red-500">
Error: {error.message}
</div>
);
}
// Check if we have results
if (!data?.flipResults?.length) {
return <div className="text-center py-8 text-gray-500">No flips found</div>;
}return (
<div className="space-y-4">
<div className="overflow-x-auto"> {/* Makes table scrollable on mobile */}
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2">Player</th>
<th className="text-left py-2">Bet</th>
<th className="text-left py-2">Choice</th>
<th className="text-left py-2">Result</th>
<th className="text-left py-2">Payout</th>
<th className="text-left py-2">Time</th>
</tr>
</thead>
<tbody>
{data.flipResults.map((flip: any) => (
<tr key={flip.id} className="border-b">
{/* Player address - truncated for readability */}
<td className="py-2 font-mono text-xs">
{truncateHash(flip.player)}
</td>
{/* Bet amount - converted from wei to ether */}
<td className="py-2">
{formatEther(flip.betAmount)} STT
</td>
{/* Player's choice - color coded */}
<td className="py-2">
<span className={
flip.choice === 'HEADS'
? 'text-blue-600' // Blue for heads
: 'text-purple-600' // Purple for tails
}>
{flip.choice}
</span>
</td>
{/* Actual result - same color coding */}
<td className="py-2">
<span className={
flip.result === 'HEADS'
? 'text-blue-600'
: 'text-purple-600'
}>
{flip.result}
</span>
</td>
{/* Payout - green if won, gray if lost */}
<td className="py-2">
<span className={
flip.payout !== '0'
? 'text-green-600' // Won
: 'text-gray-400' // Lost
}>
{flip.payout !== '0'
? `+${formatEther(flip.payout)}`
: '0'
} STT
</span>
</td>
{/* Timestamp - converted to readable date */}
<td className="py-2 text-xs text-gray-500">
{formatTime(flip.blockTimestamp)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-between">
{/* Previous button */}
<button
onClick={() => setPage(Math.max(0, page - 1))}
disabled={page === 0}
className="px-4 py-2 text-sm bg-gray-100 rounded disabled:opacity-50"
>
Previous
</button>
{/* Current page indicator */}
<span className="py-2 text-sm text-gray-600">
Page {page + 1}
</span>
{/* Next button */}
<button
onClick={() => setPage(page + 1)}
disabled={data.flipResults.length < itemsPerPage}
className="px-4 py-2 text-sm bg-gray-100 rounded disabled:opacity-50"
>
Next
</button>
</div>
</div>
);
}'use client';
import { useQuery } from '@apollo/client';
import { GET_RECENT_FLIPS } from '@/lib/queries';const truncateHash = (hash: string) => {
return `${hash.slice(0, 6)}...${hash.slice(-4)}`;
};
const formatEther = (wei: string) => {
return (parseFloat(wei) / 1e18).toFixed(4);
};export default function LiveFeed() {
// Execute query with automatic polling
const { loading, error, data } = useQuery(GET_RECENT_FLIPS, {
variables: {
first: 10 // Get 10 most recent flips
},
pollInterval: 5000, // Refresh every 5 seconds (5000ms)
});// Same loading/error handling as AllFlips
if (loading) {
return <div className="text-center py-8 text-gray-500">Loading...</div>;
}
if (error) {
return <div className="text-center py-8 text-red-500">Error: {error.message}</div>;
}
if (!data?.flipResults?.length) {
return <div className="text-center py-8 text-gray-500">No recent flips</div>;
}
'use client';
import { useQuery } from '@apollo/client';
import { GET_RECENT_FLIPS } from '@/lib/queries';
const truncateHash = (hash: string) => `${hash.slice(0, 6)}...${hash.slice(-4)}`;
const formatEther = (wei: string) => (parseFloat(wei) / 1e18).toFixed(4);
export default function LiveFeed() {
const { loading, error, data } = useQuery(GET_RECENT_FLIPS, {
variables: { first: 10 },
pollInterval: 5000,
});
if (loading) return <div className="text-center py-8 text-gray-500">Loading...</div>;
if (error) return <div className="text-center py-8 text-red-500">Error: {error.message}</div>;
if (!data?.flipResults?.length) return <div className="text-center py-8 text-gray-500">No recent flips</div>;
return (
<div className="space-y-2">
{data.flipResults.map((flip: any) => {
const won = flip.payout !== '0';
return (
<div key={flip.id} className={`p-3 rounded border ${won ? 'border-green-200 bg-green-50' : 'border-gray-200'}`}>
<div className="flex justify-between items-center">
<div>
<span className="font-mono text-sm">{truncateHash(flip.player)}</span>
<span className="text-sm text-gray-500 ml-2">bet {formatEther(flip.betAmount)} STT</span>
</div>
<div className="text-right">
<div className="text-sm">
<span className={flip.choice === 'HEADS' ? 'text-blue-600' : 'text-purple-600'}>
{flip.choice}
</span>
<span className="mx-1">→</span>
<span className={flip.result === 'HEADS' ? 'text-blue-600' : 'text-purple-600'}>
{flip.result}
</span>
</div>
<div className={`text-sm font-semibold ${won ? 'text-green-600' : 'text-gray-400'}`}>
{won ? `Won ${formatEther(flip.payout)} STT` : 'Lost'}
</div>
</div>
</div>
</div>
);
})}
<p className="text-center text-xs text-gray-500 pt-2">Auto-refreshing every 5 seconds</p>
</div>
);
}'use client';
import { useState } from 'react';
import AllFlips from '@/components/AllFlips';
import LiveFeed from '@/components/LiveFeed';
export default function Home() {
const [activeTab, setActiveTab] = useState('allFlips');
return (
<div className="max-w-4xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">SomFlip</h1>
{/* Tab Navigation */}
<div className="flex gap-4 mb-6 border-b">
<button
onClick={() => setActiveTab('allFlips')}
className={`pb-2 px-1 ${
activeTab === 'allFlips'
? 'border-b-2 border-black font-semibold'
: 'text-gray-500'
}`}
>
All Flips
</button>
<button
onClick={() => setActiveTab('liveFeed')}
className={`pb-2 px-1 ${
activeTab === 'liveFeed'
? 'border-b-2 border-black font-semibold'
: 'text-gray-500'
}`}
>
Live Feed
</button>
</div>
{/* Conditional Rendering Based on Active Tab */}
{activeTab === 'allFlips' ? <AllFlips /> : <LiveFeed />}
</div>
);
}npm run devnpx create-next-app my-dapp-uinpm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -pmodule.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}@tailwind base;
@tailwind components;
@tailwind utilities;import { createContext, useContext, useState } from "react";
const WalletContext = createContext();
export function WalletProvider({ children }) {
const [connected, setConnected] = useState(false);
const [address, setAddress] = useState("");
async function connectToMetaMask() {
if (typeof window !== "undefined" && window.ethereum) {
try {
await window.ethereum.request({ method: "eth_requestAccounts" });
// For simplicity, get the first address
const [userAddress] = window.ethereum.selectedAddress
? [window.ethereum.selectedAddress]
: [];
setAddress(userAddress);
setConnected(true);
} catch (err) {
console.error("User denied account access:", err);
}
} else {
console.log("MetaMask is not installed!");
}
}
function disconnectWallet() {
setConnected(false);
setAddress("");
}
// Return the context provider
return (
<WalletContext.Provider
value={{
connected,
address,
connectToMetaMask,
disconnectWallet,
}}
>
{children}
</WalletContext.Provider>
);
}
export function useWallet() {
return useContext(WalletContext);
}
const { connected, ... } = useWallet()import "../styles/globals.css";
import { WalletProvider } from "../contexts/walletcontext";
import NavBar from "../components/navbar";
function MyApp({ Component, pageProps }) {
return (
<WalletProvider>
<NavBar />
<main className="pt-16">
<Component {...pageProps} />
</main>
</WalletProvider>
);
}
export default MyApp;import { useWallet } from "../contexts/walletcontext";
import Link from "next/link";
export default function NavBar() {
const { connected, address, disconnectWallet } = useWallet();
return (
<nav className="fixed w-full bg-white shadow z-50">
<div className="mx-auto max-w-7xl px-4 flex h-16 items-center justify-between">
<Link href="/">
<h1 className="text-xl font-bold text-blue-600">MyDAO</h1>
</Link>
<div>
{connected ? (
<div className="flex items-center space-x-4 text-blue-500">
<span>{address.slice(0, 6)}...{address.slice(-4)}</span>
<button onClick={disconnectWallet} className="px-4 py-2 bg-red-500 text-white rounded">
Logout
</button>
</div>
) : (
<span className="text-gray-500">Not connected</span>
)}
</div>
</div>
</nav>
);
}npm run devEID : 30380



https://api.subgraph.somnia.network/public_api/data_api/somnia/v1/address/{walletAddress}/balance/erc20Authorization: Bearer YOUR_API_KEYcurl -X GET "https://api.subgraph.somnia.network/public_api/data_api/somnia/v1/address/0xYOUR_WALLET_ADDRESS/balance/erc20" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json"npx create-next-app@latest somnia-balance-demo --typescript --tailwind --app
cd somnia-balance-demo'use client'
import { useState, FormEvent } from 'react'
// Type definitions for the API response
interface TokenBalance {
balance: string
contract: {
address: string
decimals: number
erc_type: string
logoUri: string | null
name: string
symbol: string
}
raw_balance: string
}
interface BalanceResponse {
erc20TokenBalances: TokenBalance[]
resultCount: number
}export default function Home() {
const [walletAddress, setWalletAddress] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<BalanceResponse | null>(null)
const [error, setError] = useState<string>('')
return (
<main className="min-h-screen bg-white p-8">
<div className="max-w-6xl mx-auto">
<h1 className="text-3xl font-bold mb-8">Somnia Network Balance Demo</h1>
<form className="mb-8">
<div className="flex gap-4">
<input
type="text"
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
placeholder="Enter wallet address (0x...)"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={loading}
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Loading...' : 'Fetch Balance'}
</button>
</div>
</form>
</div>
</main>
)
}PRIVATE_KEY=YOUR_API_KEY_HEREimport { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const walletAddress = searchParams.get('address');
if (!walletAddress) {
return NextResponse.json(
{ error: 'Wallet address is required' },
{ status: 400 }
)
}
const apiKey = process.env.PRIVATE_KEY
const baseUrl = 'https://api.subgraph.somnia.network/public_api/data_api'
const response = await fetch(
`${baseUrl}/somnia/v1/address/${walletAddress}/balance/erc20`,
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
}
)
const data = await response.json()
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch data from Ormi API', details: data },
{ status: response.status }
)
}
return NextResponse.json(data)
} catch (error) {
console.error('API Error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}const fetchBalance = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!walletAddress) {
setError('Please enter a wallet address')
return
}
setLoading(true)
setError('')
setData(null)
try {
const response = await fetch(`/api/balance?address=${walletAddress}`, {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ walletAddress }),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Failed to fetch balance')
}
setData(result)
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
}<form onSubmit={fetchBalance} className="mb-8">{error && (
<div className="p-4 mb-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-600">{error}</p>
</div>
)}
{data && data.erc20TokenBalances.length > 0 && (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 bg-gray-50 border-b">
<h2 className="text-xl font-semibold">Token Balances ({data.resultCount} tokens)</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Symbol
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Balance
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Contract Address
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.erc20TokenBalances.map((token, index) => (
<tr key={index} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{token.contract.name || 'Unknown'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{token.contract.symbol || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{parseFloat(token.balance).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<a
href={`http://shannon-explorer.somnia.network/address/${token.contract.address}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 font-mono"
>
{token.contract.address.slice(0, 6)}...{token.contract.address.slice(-4)}
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{data && data.erc20TokenBalances.length === 0 && (
<div className="bg-gray-50 p-6 rounded-md text-center">
<p className="text-gray-600">No ERC-20 tokens found for this address</p>
</div>
)}npm run dev'use client';
import { useState, FormEvent } from 'react';
// Type definitions for the API response
interface TokenBalance {
balance: string;
contract: {
address: string;
decimals: number;
erc_type: string;
logoUri: string | null;
name: string;
symbol: string;
};
raw_balance: string;
}
interface BalanceResponse {
erc20TokenBalances: TokenBalance[];
resultCount: number;
}
export default function Home() {
const [walletAddress, setWalletAddress] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<BalanceResponse | null>(null);
const [error, setError] = useState<string>('');
const fetchBalance = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!walletAddress) {
setError('Please enter a wallet address');
return;
}
setLoading(true);
setError('');
setData(null);
try {
const response = await fetch(`/api/balance?address=${walletAddress}`, {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ walletAddress }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Failed to fetch balance');
}
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
return (
<main className='min-h-screen bg-white p-8'>
<div className='max-w-6xl mx-auto'>
<h1 className='text-3xl font-bold mb-8 text-gray-900'>
Somnia Network Balance Demo
</h1>
<form onSubmit={fetchBalance} className='mb-8'>
<div className='flex gap-4'>
<input
type='text'
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
placeholder='Enter wallet address (0x...)'
className='flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-500'
/>
<button
type='submit'
disabled={loading}
className='px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed'
>
{loading ? 'Loading...' : 'Fetch Balance'}
</button>
</div>
</form>
{error && (
<div className='p-4 mb-4 bg-red-50 border border-red-200 rounded-md'>
<p className='text-red-600'>{error}</p>
</div>
)}
{data && data.erc20TokenBalances.length > 0 && (
<div className='bg-white rounded-lg shadow overflow-hidden'>
<div className='px-6 py-4 bg-gray-50 border-b'>
<h2 className='text-xl font-semibold text-gray-900'>
Token Balances ({data.resultCount} tokens)
</h2>
</div>
<div className='overflow-x-auto'>
<table className='min-w-full divide-y divide-gray-200'>
<thead className='bg-gray-50'>
<tr>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-900 uppercase tracking-wider'>
Name
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
Symbol
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
Balance
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
Contract Address
</th>
</tr>
</thead>
<tbody className='bg-white divide-y divide-gray-200'>
{data.erc20TokenBalances.map((token, index) => (
<tr key={index} className='hover:bg-gray-50'>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{token.contract.name || 'Unknown'}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{token.contract.symbol || '-'}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{parseFloat(token.balance).toLocaleString()}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm'>
<a
href={`http://shannon-explorer.somnia.network/address/${token.contract.address}`}
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 font-mono'
>
{token.contract.address.slice(0, 6)}...
{token.contract.address.slice(-4)}
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{data && data.erc20TokenBalances.length === 0 && (
<div className='bg-gray-50 p-6 rounded-md text-center'>
<p className='text-gray-600'>
No ERC-20 tokens found for this address
</p>
</div>
)}
</div>
</main>
);
}
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const walletAddress = searchParams.get('address');
if (!walletAddress) {
return NextResponse.json(
{ error: 'Wallet address is required' },
{ status: 400 }
);
}
const apiKey = process.env.PRIVATE_KEY;
const baseUrl = 'https://api.subgraph.somnia.network/public_api/data_api';
const response = await fetch(
`${baseUrl}/somnia/v1/address/${walletAddress}/balance/erc20`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
const data = await response.json();
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch data from Ormi API', details: data },
{ status: response.status }
);
}
return NextResponse.json(data);
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
npm install -g @graphprotocol/graph-cligraph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytokentype Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes!
to: Bytes!
value: BigInt!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}graph codegen && graph buildgraph deploy mytoken --node https://api.subgraph.somnia.network/deploy --ipfs https://api.subgraph.somnia.network/ipfs --deploy-key yourORMIPrivateKey// src/lib/chatService.ts
import { SDK, SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { getPublicHttpClient, getWalletClient } from './clients'
import { waitForTransactionReceipt } from 'viem/actions'
import { toHex, type Hex } from 'viem'
import { chatSchema } from './chatSchema'
const encoder = new SchemaEncoder(chatSchema)
export async function sendMessage(room: string, content: string, senderName: string) {
const sdk = new SDK({
public: getPublicHttpClient(),
wallet: getWalletClient(),
})
// Compute or register schema
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
const isRegistered = await sdk.streams.isDataSchemaRegistered(schemaId)
if (!isRegistered) {
const ignoreAlreadyRegistered = true
const txHash = await sdk.streams.registerDataSchemas(
[{ id: 'chat', schema: chatSchema, parentSchemaId: zeroBytes32 }],
ignoreAlreadyRegistered
)
if (!txHash) throw new Error('Failed to register schema')
await waitForTransactionReceipt(getPublicHttpClient(), { hash: txHash })
}
const now = Date.now().toString()
const roomId = toHex(room, { size: 32 })
const data: Hex = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'roomId', value: roomId, type: 'bytes32' },
{ name: 'content', value: content, type: 'string' },
{ name: 'senderName', value: senderName, type: 'string' },
{ name: 'sender', value: getWalletClient().account.address, type: 'address' },
])
const dataId = toHex(`${room}-${now}`, { size: 32 })
const tx = await sdk.streams.set([{ id: dataId, schemaId, data }])
if (!tx) throw new Error('Failed to publish chat message')
await waitForTransactionReceipt(getPublicHttpClient(), { hash: tx })
return { txHash: tx }
}
'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
import { SDK } from '@somnia-chain/streams'
import { getPublicHttpClient } from './clients'
import { chatSchema } from './chatSchema'
import { toHex, type Hex } from 'viem'
// Helper to unwrap field values
const val = (f: any) => f?.value?.value ?? f?.value
// Message type
export type ChatMsg = {
timestamp: number
roomId: `0x${string}`
content: string
senderName: string
sender: `0x${string}`
}
/**
* Fetch chat messages from Somnia Streams (read-only, auto-refresh, cumulative)
*/
export function useChatMessages(
roomName?: string,
limit = 100,
refreshMs = 5000
) {
const [messages, setMessages] = useState<ChatMsg[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const timerRef = useRef<NodeJS.Timeout | null>(null)
const loadMessages = useCallback(async () => {
try {
const sdk = new SDK({ public: getPublicHttpClient() })
// Compute schema ID from the chat schema
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
const publisher =
process.env.NEXT_PUBLIC_PUBLISHER_ADDRESS ??
'0x0000000000000000000000000000000000000000'
// Fetch all publisher data for schema
const resp = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)
// Ensure array structure (each row corresponds to an array of fields)
const rows: any[][] = Array.isArray(resp) ? (resp as any[][]) : []
if (!rows.length) {
setMessages([])
setLoading(false)
return
}
// Convert room name to bytes32 for filtering (if applicable)
const want = roomName ? toHex(roomName, { size: 32 }).toLowerCase() : null
const parsed: ChatMsg[] = []
for (const row of rows) {
if (!Array.isArray(row) || row.length < 5) continue
const ts = Number(val(row[0]))
const ms = String(ts).length <= 10 ? ts * 1000 : ts // handle seconds vs ms
const rid = String(val(row[1])) as `0x${string}`
// Skip messages from other rooms if filtered
if (want && rid.toLowerCase() !== want) continue
parsed.push({
timestamp: ms,
roomId: rid,
content: String(val(row[2]) ?? ''),
senderName: String(val(row[3]) ?? ''),
sender: (String(val(row[4])) as `0x${string}`) ??
'0x0000000000000000000000000000000000000000',
})
}
// Sort by timestamp (ascending)
parsed.sort((a, b) => a.timestamp - b.timestamp)
// Deduplicate and limit
setMessages((prev) => {
const combined = [...prev, ...parsed]
const unique = combined.filter(
(msg, index, self) =>
index ===
self.findIndex(
(m) =>
m.timestamp === msg.timestamp &&
m.sender === msg.sender &&
m.content === msg.content
)
)
return unique.slice(-limit)
})
setError(null)
} catch (err: any) {
console.error('❌ Failed to load chat messages:', err)
setError(err.message || 'Failed to load messages')
} finally {
setLoading(false)
}
}, [roomName, limit])
// Initial load + polling
useEffect(() => {
setLoading(true)
loadMessages()
timerRef.current = setInterval(loadMessages, refreshMs)
return () => timerRef.current && clearInterval(timerRef.current)
}, [loadMessages, refreshMs])
return { messages, loading, error, reload: loadMessages }
}
'use client'
import { useState } from 'react'
import { useChatMessages } from '@/lib/chatMessages'
export default function Page() {
const [room, setRoom] = useState('general')
const [content, setContent] = useState('')
const [senderName, setSenderName] = useState('Victory')
const [error, setError] = useState<string | null>(null)
const {
messages,
loading,
error: fetchError,
reload,
} = useChatMessages(room, 200)
// --- Send new message via API route ---
async function send() {
try {
if (!content.trim()) {
setError('Message content cannot be empty')
return
}
const res = await fetch('/api/send', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ room, content, senderName }),
})
const data = await res.json()
if (!res.ok) throw new Error(data?.error || 'Failed to send message')
setContent('')
setError(null)
reload() // refresh after sending
} catch (e: any) {
console.error('❌ Send message failed:', e)
setError(e?.message || 'Failed to send message')
}
}
// --- Render UI ---
return (
<main
style={{
padding: 24,
fontFamily: 'system-ui, sans-serif',
maxWidth: 640,
margin: '0 auto',
}}
>
<h1>💬 Somnia Data Streams Chat</h1>
<p style={{ color: '#666' }}>
Messages are stored <b>onchain</b> and read using Somnia Data Streams.
</p>
{/* Room + Name inputs */}
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<input
value={room}
onChange={(e) => setRoom(e.target.value)}
placeholder="room name"
style={{ flex: 1, padding: 6 }}
/>
<input
value={senderName}
onChange={(e) => setSenderName(e.target.value)}
placeholder="your name"
style={{ flex: 1, padding: 6 }}
/>
<button
onClick={reload}
disabled={loading}
style={{
background: '#0070f3',
color: 'white',
border: 'none',
padding: '6px 12px',
cursor: 'pointer',
borderRadius: 4,
}}
>
Refresh
</button>
</div>
{/* Message input */}
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<input
style={{ flex: 1, padding: 6 }}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Type your message..."
/>
<button
onClick={send}
style={{
background: '#28a745',
color: 'white',
border: 'none',
padding: '6px 12px',
cursor: 'pointer',
borderRadius: 4,
}}
>
Send
</button>
</div>
{/* Error messages */}
{(error || fetchError) && (
<div style={{ color: 'crimson', marginBottom: 12 }}>
Error: {error || fetchError}
</div>
)}
{/* Message list */}
{loading ? (
<p>Loading messages...</p>
) : !messages.length ? (
<p>No messages yet.</p>
) : (
<ul style={{ paddingLeft: 16, listStyle: 'none' }}>
{messages.map((m, i) => (
<li key={i} style={{ marginBottom: 8 }}>
<small style={{ color: '#666' }}>
{new Date(m.timestamp).toLocaleTimeString()}
</small>{' '}
<b>{m.senderName || m.sender}</b>: {m.content}
</li>
))}
</ul>
)}
</main>
)
}
npx create-next-app@latest somnia-chat --ts --app --no-tailwind
cd somnia-chatnpm i @somnia-chain/streams viemnpm i -D @types/nodeRPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
CHAT_PUBLISHER=0xYOUR_WALLET_ADDRESSimport { 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'],
webSocket: ['wss://dream-rpc.somnia.network/ws'] },
public: { http: ['https://dream-rpc.somnia.network'],
webSocket: ['wss://dream-rpc.somnia.network/ws'] },
},
} as const)import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount, type PrivateKeyAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'
export function getPublicHttpClient() {
return createPublicClient({
chain: somniaTestnet,
transport: http(RPC_URL),
})
}
export function getWalletClient() {
return createWalletClient({
account: privateKeyToAccount(need('PRIVATE_KEY') as `0x${string}`),
chain: somniaTestnet,
transport: http(RPC_URL),
})
}
export const publisherAddress = () => getAccount().addressexport const chatSchema = 'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'import { SDK, SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { getPublicHttpClient, getWalletClient, publisherAddress } from './clients'
import { waitForTransactionReceipt } from 'viem/actions'
import { toHex, type Hex } from 'viem'
import { chatSchema } from './chatSchema'
const encoder = new SchemaEncoder(chatSchema)
const sdk = new SDK({
public: getPublicHttpClient(),
wallet: getWalletClient(),
})// Register schema
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
const isRegistered = await sdk.streams.isDataSchemaRegistered(schemaId)
if (!isRegistered) {
const ignoreAlreadyRegistered = true
const txHash = await sdk.streams.registerDataSchemas(
[{ schemaName: 'chat', schema: chatSchema, parentSchemaId: zeroBytes32 }],
ignoreAlreadyRegistered
)
if (!txHash) throw new Error('Failed to register schema')
await waitForTransactionReceipt(getPublicHttpClient(), { hash: txHash })
}const now = Date.now().toString()
const roomId = toHex(room, { size: 32 })
const data: Hex = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'roomId', value: roomId, type: 'bytes32' },
{ name: 'content', value: content, type: 'string' },
{ name: 'senderName', value: senderName, type: 'string' },
{ name: 'sender', value: getWalletClient().account.address, type: 'address' },
])
const dataId = toHex(`${room}-${now}`, { size: 32 })
const tx = await sdk.streams.set([{ id: dataId, schemaId, data }])
if (!tx) throw new Error('Failed to publish chat message')
await waitForTransactionReceipt(getPublicHttpClient(), { hash: tx })
return { txHash: tx }import { NextResponse } from 'next/server'
import { sendMessage } from '@/lib/chatService'
export async function POST(req: Request) {
try {
const { room, content, senderName } = await req.json()
if (!room || !content) throw new Error('Missing fields')
const { txHash } = await sendMessage(room, content, senderName)
return NextResponse.json({ success: true, txHash })
} catch (e: any) {
console.error(e)
return NextResponse.json({ error: e.message || 'Failed to send' }, { status: 500 })
}
}use client'
import { useState } from 'react'
import { useChatMessages } from '@/lib/chatMessages'
export default function Page() {
const [room, setRoom] = useState('general')
const [content, setContent] = useState('')
const [senderName, setSenderName] = useState('Victory')
const [error, setError] = useState<string | null>(null)
const {
messages,
loading,
error: fetchError,
reload,
} = useChatMessages(room, 200)
// --- Send new message via API route ---
async function send() {
try {
if (!content.trim()) {
setError('Message content cannot be empty')
return
}
const res = await fetch('/api/send', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ room, content, senderName }),
})
const data = await res.json()
if (!res.ok) throw new Error(data?.error || 'Failed to send message')
setContent('')
setError(null)
reload() // refresh after sending
} catch (e: any) {
console.error('❌ Send message failed:', e)
setError(e?.message || 'Failed to send message')
}
} return (
<main style={{ padding: 24, fontFamily: 'system-ui, sans-serif' }}>
<h1>💬 Somnia Streams Chat (read-only)</h1>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<input
value={room}
onChange={(e) => setRoom(e.target.value)}
placeholder="room"
/>
<input
value={senderName}
onChange={(e) => setSenderName(e.target.value)}
placeholder="name"
/>
<button onClick={reload} disabled={loading}>
Refresh
</button>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<input
style={{ flex: 1 }}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Type a message"
/>
<button onClick={send}>Send</button>
</div>
{(error || fetchError) && (
<div style={{ color: 'crimson', marginBottom: 12 }}>
Error: {error || fetchError}
</div>
)}
{loading ? (
<p>Loading messages...</p>
) : !messages.length ? (
<p>No messages yet.</p>
) : (
<ul style={{ paddingLeft: 16 }}>
{messages.map((m, i) => (
<li key={i}>
<small>{new Date(m.timestamp).toLocaleTimeString()} </small>
<b>{m.senderName || m.sender}</b>: {m.content}
</li>
))}
</ul>
)}
</main>
)
}+-----------+ +--------------------+ +---------------------+
| User UI | ---> | Next.js API /send | ---> | Somnia Data Streams |
+-----------+ +--------------------+ +---------------------+
^ | |
| v |
| +------------------+ |
| | Blockchain | |
| | (Transaction) | |
| +------------------+ |
| | |
|<----------------------| |
| Poll via SDK or Subscribe (useChatMessages) |
+---------------------------------------------------+
npm run dev# .env
RPC_URL_SOMNIA=[https://dream-rpc.somnia.network]
RPC_URL_SEPOLIA=[https://sepolia.drpc.org]
PRIVATE_KEY_SOMNIA=0xYOUR_SOMNIA_PRIVATE_KEYnpm i @somnia-chain/streams viem dotenv
npm i -D @types/node typescript ts-nodeimport { 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 = sepoliaBaseimport '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')),
})// This schema will store historical price snapshots
export const priceFeedSchema =
'uint64 timestamp, int256 price, uint80 roundId, string pair'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
}
}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 and Register (idempotent)
const schemaId = await sdk.streams.computeSchemaId(priceFeedSchema)
if (!schemaId) throw new Error('Could not compute schemaId')
const ignoreAlreadyRegisteredSchemas = true
const regTx = await sdk.streams.registerDataSchemas([
{ id: 'price-feed-v1', schema: priceFeedSchema }
], ignoreAlreadyRegisteredSchemas)
if (!regTx) throw new Error('Failed to register schema')
await waitForTransactionReceipt(sdk.public, { hash: regTx })
// 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)
})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)
})--- 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...)
AMMs are fully decentralized with no need for counterparties and are Open and permissionless to use and contribute liquidity.
// IERC20.sol
pragma solidity ^0.8.0;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// SomniaPair.sol
pragma solidity ^0.8.0;
import "./IERC20.sol";
contract SomniaPair is IERC20 {
uint256 public constant MINIMUM_LIQUIDITY = 10**3;
address public factory;
address public token0;
address public token1;
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
uint256 public kLast;
uint256 private unlocked = 1;
modifier lock() {
require(unlocked == 1, 'LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
// ERC-20 Implementation
string public constant name = "Somnia LP Token";
string public constant symbol = "SLP";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
event Swap(
address indexed sender,
uint256 amount0In,
uint256 amount1In,
uint256 amount0Out,
uint256 amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
constructor() {
factory = msg.sender;
}
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'FORBIDDEN');
token0 = _token0;
token1 = _token1;
}
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
function _safeTransfer(address token, address to, uint256 value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FAILED');
}
function _update(uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, 'OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
function mint(address to) external lock returns (uint256 liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 amount0 = balance0 - _reserve0;
uint256 amount1 = balance1 - _reserve1;
uint256 _totalSupply = totalSupply;
if (_totalSupply == 0) {
liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
liquidity = min(amount0 * _totalSupply / _reserve0, amount1 * _totalSupply / _reserve1);
}
require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
kLast = uint256(reserve0) * reserve1;
emit Mint(msg.sender, amount0, amount1);
}
function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
address _token0 = token0;
address _token1 = token1;
uint256 balance0 = IERC20(_token0).balanceOf(address(this));
uint256 balance1 = IERC20(_token1).balanceOf(address(this));
uint256 liquidity = balanceOf[address(this)];
uint256 _totalSupply = totalSupply;
amount0 = liquidity * balance0 / _totalSupply;
amount1 = liquidity * balance1 / _totalSupply;
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
kLast = uint256(reserve0) * reserve1;
emit Burn(msg.sender, amount0, amount1, to);
}
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'INSUFFICIENT_LIQUIDITY');
uint256 balance0;
uint256 balance1;
{
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'INSUFFICIENT_INPUT_AMOUNT');
{
uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3;
uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3;
require(balance0Adjusted * balance1Adjusted >= uint256(_reserve0) * _reserve1 * 1000**2, 'K');
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
// Helper functions
function sqrt(uint256 y) internal pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = x < y ? x : y;
}
// ERC-20 functions
function _mint(address to, uint256 value) internal {
totalSupply += value;
balanceOf[to] += value;
emit Transfer(address(0), to, value);
}
function _burn(address from, uint256 value) internal {
balanceOf[from] -= value;
totalSupply -= value;
emit Transfer(from, address(0), value);
}
function approve(address spender, uint256 value) external returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transfer(address to, uint256 value) external returns (bool) {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
if (allowance[from][msg.sender] != type(uint256).max) {
allowance[from][msg.sender] -= value;
}
balanceOf[from] -= value;
balanceOf[to] += value;
emit Transfer(from, to, value);
return true;
}
}
// SomniaFactory.sol
pragma solidity ^0.8.0;
import "./SomniaPair.sol";
contract SomniaFactory {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
event PairCreated(address indexed token0, address indexed token1, address pair, uint256);
function allPairsLength() external view returns (uint256) {
return allPairs.length;
}
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'PAIR_EXISTS');
bytes memory bytecode = type(SomniaPair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
SomniaPair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair;
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
}
// SomniaRouter.sol
pragma solidity ^0.8.0;
import "./IERC20.sol";
import "./SomniaPair.sol";
import "./SomniaFactory.sol";
contract SomniaRouter {
address public immutable factory;
modifier ensure(uint256 deadline) {
require(deadline >= block.timestamp, 'EXPIRED');
_;
}
constructor(address _factory) {
factory = _factory;
}
// Add liquidity
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = pairFor(tokenA, tokenB);
_safeTransferFrom(tokenA, msg.sender, pair, amountA);
_safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = SomniaPair(pair).mint(to);
}
function _addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin
) internal returns (uint256 amountA, uint256 amountB) {
if (SomniaFactory(factory).getPair(tokenA, tokenB) == address(0)) {
SomniaFactory(factory).createPair(tokenA, tokenB);
}
(uint256 reserveA, uint256 reserveB) = getReserves(tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
// Remove liquidity
function removeLiquidity(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) public ensure(deadline) returns (uint256 amountA, uint256 amountB) {
address pair = pairFor(tokenA, tokenB);
SomniaPair(pair).transferFrom(msg.sender, pair, liquidity);
(uint256 amount0, uint256 amount1) = SomniaPair(pair).burn(to);
(address token0,) = sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
}
// Swap functions
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256[] memory amounts) {
amounts = getAmountsOut(amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
_safeTransferFrom(
path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
function swapTokensForExactTokens(
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256[] memory amounts) {
amounts = getAmountsIn(amountOut, path);
require(amounts[0] <= amountInMax, 'EXCESSIVE_INPUT_AMOUNT');
_safeTransferFrom(
path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
// Internal functions
function _swap(uint256[] memory amounts, address[] memory path, address _to) internal {
for (uint256 i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = sortTokens(input, output);
uint256 amountOut = amounts[i + 1];
(uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));
address to = i < path.length - 2 ? pairFor(output, path[i + 2]) : _to;
SomniaPair(pairFor(input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
// Library functions
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'ZERO_ADDRESS');
}
function pairFor(address tokenA, address tokenB) internal view returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = SomniaFactory(factory).getPair(token0, token1);
require(pair != address(0), 'PAIR_DOES_NOT_EXIST');
}
function getReserves(address tokenA, address tokenB) internal view returns (uint256 reserveA, uint256 reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint256 reserve0, uint256 reserve1,) = SomniaPair(pairFor(tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) internal pure returns (uint256 amountB) {
require(amountA > 0, 'INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'INSUFFICIENT_LIQUIDITY');
amountB = amountA * reserveB / reserveA;
}
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountOut) {
require(amountIn > 0, 'INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountIn) {
require(amountOut > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
uint256 numerator = reserveIn * amountOut * 1000;
uint256 denominator = (reserveOut - amountOut) * 997;
amountIn = (numerator / denominator) + 1;
}
function getAmountsOut(uint256 amountIn, address[] memory path) public view returns (uint256[] memory amounts) {
require(path.length >= 2, 'INVALID_PATH');
amounts = new uint256[](path.length);
amounts[0] = amountIn;
for (uint256 i; i < path.length - 1; i++) {
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
function getAmountsIn(uint256 amountOut, address[] memory path) public view returns (uint256[] memory amounts) {
require(path.length >= 2, 'INVALID_PATH');
amounts = new uint256[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint256 i = path.length - 1; i > 0; i--) {
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
function _safeTransferFrom(address token, address from, address to, uint256 value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FROM_FAILED');
}
}
// Approve router to spend tokens
tokenA.approve(router.address, amountA);
tokenB.approve(router.address, amountB);
// Add liquidity
router.addLiquidity(
tokenA.address,
tokenB.address,
amountA,
amountB,
minAmountA,
minAmountB,
userAddress,
deadline
);// Approve router to spend input token
tokenA.approve(router.address, amountIn);
// Swap exact tokens for tokens
address[] memory path = new address[](2);
path[0] = tokenA.address;
path[1] = tokenB.address;
router.swapExactTokensForTokens(
amountIn,
minAmountOut,
path,
userAddress,
deadline
);// Approve router to spend LP tokens
pair.approve(router.address, lpTokenAmount);
// Remove liquidity
router.removeLiquidity(
tokenA.address,
tokenB.address,
lpTokenAmount,
minAmountA,
minAmountB,
userAddress,
deadline
);pragma solidity ^0.8.0;
import "./SomniaFactory.sol";
import "./SomniaRouter.sol";
import "./IERC20.sol";
contract TestDEX {
SomniaFactory public factory;
SomniaRouter public router;
constructor() {
factory = new SomniaFactory();
router = new SomniaRouter(address(factory));
}
function testCreatePair(address tokenA, address tokenB) external returns (address) {
return factory.createPair(tokenA, tokenB);
}
function testAddLiquidity(
address tokenA,
address tokenB,
uint256 amountA,
uint256 amountB
) external {
IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);
IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);
IERC20(tokenA).approve(address(router), amountA);
IERC20(tokenB).approve(address(router), amountB);
router.addLiquidity(
tokenA,
tokenB,
amountA,
amountB,
0,
0,
msg.sender,
block.timestamp + 3600
);
}
}registerDataSchemas(registrations: DataSchemaRegistration[], ignoreRegisteredSchemas?: boolean): Promise<Hex | Error | null>manageEventEmittersForRegisteredStreamsEvent(streamsEventId: string, emitter: Address, isEmitter: boolean): Promise<Hex | Error | null>getByKey(schemaId: SchemaID, publisher: Address, key: Hex): Promise<Hex[] | SchemaDecodedItem[][] | null>getAtIndex(schemaId: SchemaID, publisher: Address, idx: bigint): Promise<Hex[] | SchemaDecodedItem[][] | null>getBetweenRange(schemaId: SchemaID, publisher: Address, startIndex: bigint, endIndex: bigint): Promise<Hex[] | SchemaDecodedItem[][] | Error | null>getAllPublisherDataForSchema(schemaReference: SchemaReference, publisher: Address): Promise<Hex[] | SchemaDecodedItem[][] | null>getLastPublishedDataForSchema(schemaId: SchemaID, publisher: Address): Promise<Hex[] | SchemaDecodedItem[][] | null>getSchemaFromSchemaId(schemaId: SchemaID): Promise<{ baseSchema: string, finalSchema: string, schemaId: Hex } | Error | null>deserialiseRawData(rawData: Hex[], parentSchemaId: Hex, schemaLookup: { schema: string; schemaId: Hex; } | null): Promise<Hex[] | SchemaDecodedItem[][] | null>subscribe(initParams: SubscriptionInitParams): Promise<{ subscriptionId: string, unsubscribe: () => void } | undefined>getSomniaDataStreamsProtocolInfo(): Promise<GetSomniaDataStreamsProtocolInfoResponse | Error | null>import { parseEther } from "viem";
export function WalletProvider({ children }) {
// ...existing state and actions
// Deposit Function
const deposit = async () => {
if (!client || !address) {
alert("Please connect your wallet first!");
return;
}
try {
const tx = await client.writeContract({
address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
abi: ABI,
functionName: "deposit",
value: parseEther("0.001"), // 0.001 STT
});
console.log("Deposit Transaction:", tx);
alert("Deposit successful! Transaction hash: " + tx.hash);
} catch (error) {
console.error("Deposit failed:", error);
alert("Deposit failed. Check console for details.");
}
};
// ...other functions
return (
<WalletContext.Provider
value={{
// ...existing values
deposit,
// ...other write functions
}}
>
{children}
</WalletContext.Provider>
);
}export function WalletProvider({ children }) {
// ...existing state and actions
// Create Proposal Function
const createProposal = async (description) => {
if (!client || !address) {
alert("Please connect your wallet first!");
return;
}
try {
const tx = await client.writeContract({
address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
abi: ABI,
functionName: "createProposal",
args: [description],
});
console.log("Create Proposal Transaction:", tx);
alert("Proposal created! Transaction hash: " + tx.hash);
} catch (error) {
console.error("Create Proposal failed:", error);
alert("Failed to create proposal. Check console for details.");
}
};
// ...other functions
return (
<WalletContext.Provider
value={{
// ...existing values
createProposal,
// ...other write functions
}}
>
{children}
</WalletContext.Provider>
);
}export function WalletProvider({ children }) {
// ...existing state and actions
// Vote on Proposal Function
const voteOnProposal = async (proposalId, support) => {
if (!client || !address) {
alert("Please connect your wallet first!");
return;
}
try {
const tx = await client.writeContract({
address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
abi: ABI,
functionName: "vote",
args: [parseInt(proposalId), support],
});
console.log("Vote Transaction:", tx);
alert(`Voted ${support ? "YES" : "NO"} on proposal #${proposalId}! Transaction hash: ${tx.hash}`);
} catch (error) {
console.error("Vote failed:", error);
alert("Voting failed. Check console for details.");
}
};
// ...other functions
return (
<WalletContext.Provider
value={{
// ...existing values
voteOnProposal,
// ...other write functions
}}
>
{children}
</WalletContext.Provider>
);
}export function WalletProvider({ children }) {
// ...existing state and actions
// Execute Proposal Function
const executeProposal = async (proposalId) => {
if (!client || !address) {
alert("Please connect your wallet first!");
return;
}
try {
const tx = await client.writeContract({
address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
abi: ABI,
functionName: "executeProposal",
args: [parseInt(proposalId)],
});
console.log("Execute Proposal Transaction:", tx);
alert(`Proposal #${proposalId} executed! Transaction hash: ${tx.hash}`);
} catch (error) {
console.error("Execute Proposal failed:", error);
alert("Execution failed. Check console for details.");
}
};
// ...other functions
return (
<WalletContext.Provider
value={{
// ...existing values
executeProposal,
// ...other write functions
}}
>
{children}
</WalletContext.Provider>
);
}import { useState } from "react";
import { useRouter } from "next/router";
import { useWallet } from "../contexts/walletContext";
import { Label, TextInput, Button, Alert } from "flowbite-react";
export default function CreateProposalPage() {
const [description, setDescription] = useState("");
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState("");
const [error, setError] = useState("");
const { connected, createProposal } = useWallet();
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
setSuccess("");
if (!connected) {
setError("You must connect your wallet first!");
return;
}
if (!description.trim()) {
setError("Proposal description cannot be empty!");
return;
}
setLoading(true);
try {
await createProposal(description.trim());
setSuccess("Proposal created successfully!");
setDescription("");
// Optionally redirect to home or another page
// router.push("/");
} catch (err) {
console.error("Error creating proposal:", err);
setError("Failed to create proposal. Check console for details.");
} finally {
setLoading(false);
}
};
return (
<div className="max-w-2xl mx-auto mt-20 p-4">
<h1 className="text-2xl font-bold mb-4">Create Proposal</h1>
{error && (
<Alert color="failure" className="mb-4">
<span>
<span className="font-medium">Error!</span> {error}
</span>
</Alert>
)}
{success && (
<Alert color="success" className="mb-4">
<span>
<span className="font-medium">Success!</span> {success}
</span>
</Alert>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="proposal-description" value="Proposal Description" />
<TextInput
id="proposal-description"
type="text"
placeholder="Enter proposal description..."
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
</div>
<Button type="submit" color="purple" disabled={loading}>
{loading ? "Submitting..." : "Submit Proposal"}
</Button>
</form>
</div>
);
}import { useState } from "react";
import { useWallet } from "../contexts/walletContext";
import { Button, Card, Label, TextInput, Spinner, Alert } from "flowbite-react";
export default function FetchProposalPage() {
const [proposalId, setProposalId] = useState("");
const [proposalData, setProposalData] = useState(null);
const [loading, setLoading] = useState(false);
const [voting, setVoting] = useState(false);
const [executing, setExecuting] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const { connected, fetchProposal, voteOnProposal, executeProposal } = useWallet();
const handleFetch = async (e) => {
e.preventDefault();
setError("");
setSuccess("");
setProposalData(null);
if (!connected) {
setError("You must connect your wallet first!");
return;
}
if (!proposalId.trim()) {
setError("Please enter a proposal ID.");
return;
}
setLoading(true);
try {
const data = await fetchProposal(proposalId);
setProposalData(data);
} catch (err) {
console.error("Error fetching proposal:", err);
setError("Failed to fetch proposal. Check console for details.");
} finally {
setLoading(false);
}
};
const handleVote = async (support) => {
setError("");
setSuccess("");
setVoting(true);
try {
await voteOnProposal(proposalId, support);
setSuccess(`Successfully voted ${support ? "YES" : "NO"} on proposal #${proposalId}.`);
// Optionally, refresh the proposal data
const updatedData = await fetchProposal(proposalId);
setProposalData(updatedData);
} catch (err) {
console.error("Error voting:", err);
setError("Voting failed. Check console for details.");
} finally {
setVoting(false);
}
};
const handleExecute = async () => {
setError("");
setSuccess("");
setExecuting(true);
try {
await executeProposal(proposalId);
setSuccess(`Proposal #${proposalId} executed successfully.`);
// Optionally, refresh the proposal data
const updatedData = await fetchProposal(proposalId);
setProposalData(updatedData);
} catch (err) {
console.error("Error executing proposal:", err);
setError("Execution failed. Check console for details.");
} finally {
setExecuting(false);
}
};
return (
<div className="max-w-2xl mx-auto mt-20 p-4">
<h1 className="text-2xl font-bold mb-4">Fetch a Proposal</h1>
{/* Form to input Proposal ID */}
<form onSubmit={handleFetch} className="space-y-4">
<div>
<Label htmlFor="proposal-id" value="Proposal ID" />
<TextInput
id="proposal-id"
type="number"
placeholder="Enter proposal ID"
value={proposalId}
onChange={(e) => setProposalId(e.target.value)}
required
/>
</div>
<Button type="submit" color="blue" disabled={loading}>
{loading ? <Spinner aria-label="Loading" /> : "Fetch Proposal"}
</Button>
</form>
{/* Display Errors */}
{error && (
<Alert color="failure" className="mt-4">
<span>
<span className="font-medium">Error!</span> {error}
</span>
</Alert>
)}
{/* Display Success Messages */}
{success && (
<Alert color="success" className="mt-4">
<span>
<span className="font-medium">Success!</span> {success}
</span>
</Alert>
)}
{/* Display Proposal Details */}
{proposalData && (
<Card className="mt-8">
<h2 className="text-xl font-bold mb-2">Proposal #{proposalId}</h2>
<ul className="list-disc list-inside space-y-1">
<li>
<strong>Description:</strong> {proposalData[0]}
</li>
<li>
<strong>Deadline:</strong> {new Date(proposalData[1] * 1000).toLocaleString()}
</li>
<li>
<strong>Yes Votes:</strong> {proposalData[2].toString()}
</li>
<li>
<strong>No Votes:</strong> {proposalData[3].toString()}
</li>
<li>
<strong>Executed:</strong> {proposalData[4] ? "Yes" : "No"}
</li>
<li>
<strong>Proposer:</strong> {proposalData[5]}
</li>
</ul>
{/* Voting Buttons */}
<div className="mt-4 flex space-x-4">
<Button
color="green"
onClick={() => handleVote(true)}
disabled={voting || executing}
>
{voting ? <Spinner aria-label="Loading" size="sm" /> : "Vote YES"}
</Button>
<Button
color="red"
onClick={() => handleVote(false)}
disabled={voting || executing}
>
{voting ? <Spinner aria-label="Loading" size="sm" /> : "Vote NO"}
</Button>
</div>
{/* Execute Button */}
{!proposalData[4] && (
<div className="mt-4">
<Button
color="purple"
onClick={handleExecute}
disabled={executing || voting}
>
{executing ? <Spinner aria-label="Loading" size="sm" /> : "Execute Proposal"}
</Button>
</div>
)}
</Card>
)}
</div>
);
}npm install react-toastifyimport 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
function MyApp({ Component, pageProps }) {
return (
<WalletProvider>
<NavBar />
<main className="pt-16">
<Component {...pageProps} />
<ToastContainer />
</main>
</WalletProvider>
);
}
export default MyApp;import { toast } from 'react-toastify';
// Replace alert with toast
toast.success("Deposit successful! Transaction hash: " + tx.hash);
toast.error("Deposit failed. Check console for details.");npm run devnpm i @somnia-chain/streams viem dotenvRPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=your_private_key_hereimport { SDK } from '@somnia-chain/streams'
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from 'viem/chains'
const rpcUrl = process.env.RPC_URL!
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const sdk = new SDK({
public: createPublicClient({ chain: somniaTestnet, transport: http(rpcUrl) }),
wallet: createWalletClient({ chain: somniaTestnet, account, transport: http(rpcUrl) })
})const sdk = new SDK({
public: getPublicClient(), // for reading and subscriptions
wallet: getWalletClient() // for writing
})const tx = await sdk.streams.set([
{ id: dataId, schemaId, data }
])
console.log('Data published with tx hash:', tx)await sdk.streams.emitEvents([
{
id: 'ChatMessage',
argumentTopics: [topic],
data: '0x' // optional encoded payload
}
])await sdk.streams.setAndEmitEvents(
[{ id: dataId, schemaId, data }],
[{ id: 'ChatMessage', argumentTopics: [topic], data: '0x' }]
)await sdk.streams.registerDataSchemas([
{
schemaName: "chat",
schema: 'uint64 timestamp, string message, address sender',
parentSchemaId: zeroBytes32 // root schema
}
], true) // Optionally ignore if already registeredawait sdk.streams.registerEventSchemas(
['ChatMessage'],
[{
params: [{ name: 'roomId', paramType: 'bytes32', isIndexed: true }],
eventTopic: 'ChatMessage(bytes32 indexed roomId)'
}]
)await sdk.streams.manageEventEmittersForRegisteredStreamsEvent(
'ChatMessage',
'0x1234abcd...',
true // allow this address to emit
)const msg = await sdk.streams.getByKey(schemaId, publisher, dataId)
console.log('Data:', msg)const record = await sdk.streams.getAtIndex(schemaId, publisher, 0n)const records = await sdk.streams.getBetweenRange(schemaId, publisher, 0n, 10n)
console.log('Records in range:', records)const allData = await sdk.streams.getAllPublisherDataForSchema(schemaReference, publisher)
console.log('All publisher data:', allData)const latest = await sdk.streams.getLastPublishedDataForSchema(schemaId, publisher)
console.log('Latest data:', latest)const total = await sdk.streams.totalPublisherDataForSchema(schemaId, publisher)
console.log(`Total entries: ${total}`)const exists = await sdk.streams.isDataSchemaRegistered(schemaId)
if (!exists) console.log('Schema not found')const parent = await sdk.streams.parentSchemaId(schemaId)
console.log('Parent Schema ID:', parent)const id = await sdk.streams.schemaIdToId(schemaId)
console.log('Schema ID string:', id)const schemaId = await sdk.streams.idToSchemaId('chat')
console.log('Schema ID:', schemaId)const schemas = await sdk.streams.getAllSchemas()
console.log('All schemas:', schemas)const eventSchemas = await sdk.streams.getEventSchemasById(['ChatMessage'])
console.log('Event schemas:', eventSchemas)const schemaId = await sdk.streams.computeSchemaId('uint64 timestamp, string content')const schemaInfo = await sdk.streams.getSchemaFromSchemaId(schemaId)
console.log('Schema info:', schemaInfo)const decoded = await sdk.streams.deserialiseRawData(rawData, parentSchemaId, schemaLookup)
console.log('Decoded data:', decoded)// Wildcard subscription to all events emitted by all contracts
await sdk.streams.subscribe({
ethCalls: [], // No view calls
onData: (data) => {}
})import { toEventSelector } from "viem"
const transferSelector = toEventSelector({
name: 'Transfer',
type: 'event',
inputs: [
{ type: 'address', indexed: true, name: 'from' },
{ type: 'address', indexed: true, name: 'to' },
{ type: 'uint256', indexed: false, name: 'value' }
]
})
await sdk.streams.subscribe({
topicOverrides: [
transferSelector, // Topic 0 (Transfer event)
],
ethCalls: [{
to: '0xERC20Address',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'balanceOf',
args: ['0xUserAddress']
})
}],
onData: (data) => console.log('Trade + balance data:', data)
})const info = await sdk.streams.getSomniaDataStreamsProtocolInfo()
console.log('Protocol info:', info)// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
/**
* @title ReentrancyVulnerable
* @notice Classic vulnerable withdraw pattern for tutorial exploitation in Remix.
* @dev Demonstrates why updating state after external calls is dangerous.
*
* WARNING: Use only in local/Remix test environments. Do NOT deploy vulnerable contracts with real funds on mainnet.
*/
contract ReentrancyVulnerable {
mapping(address => uint256) public deposits;
event Deposited(address indexed who, uint256 amount);
event Withdrawn(address indexed who, uint256 amount);
function deposit() external payable {
require(msg.value > 0, "zero deposit");
deposits[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
require(deposits[msg.sender] >= amount, "insufficient balance");
// INTERACTION before EFFECTS -> vulnerable to reentrancy
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "transfer failed");
// EFFECTS: update balance after external call -> attacker can re-enter here
deposits[msg.sender] -= amount;
emit Withdrawn(msg.sender, amount);
}
function contractBalance() external view returns (uint256) {
return address(this).balance;
}
}contract Attacker {
ReentrancyVulnerable target;
uint256 public constant ATTACK_AMOUNT = 1 ether;
constructor(address _target) {
target = ReentrancyVulnerable(_target);
}
function attack() external payable {
require(msg.value >= ATTACK_AMOUNT, "need at least 1 ETH");
target.deposit{value: ATTACK_AMOUNT}();
target.withdraw(ATTACK_AMOUNT);
}
receive() external payable {
if (address(target).balance >= ATTACK_AMOUNT) {
target.withdraw(ATTACK_AMOUNT);
}
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ReentrancySecure {
mapping(address => uint256) private _deposits;
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status = _NOT_ENTERED;
event Deposited(address indexed who, uint256 amount);
event Withdrawn(address indexed who, uint256 amount);
modifier nonReentrant() {
require(_status == _NOT_ENTERED, "reentrant");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
function deposit() external payable {
require(msg.value > 0, "zero deposit");
_deposits[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint256 amount) external nonReentrant {
uint256 bal = _deposits[msg.sender];
require(bal >= amount, "insufficient balance");
// EFFECTS
_deposits[msg.sender] = bal - amount;
// INTERACTIONS
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "transfer failed");
emit Withdrawn(msg.sender, amount);
}
function depositOf(address who) external view returns (uint256) {
return _deposits[who];
}
function contractBalance() external view returns (uint256) {
return address(this).balance;
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AccessControlVulnerable {
mapping(bytes32 => mapping(address => bool)) public roles;
bytes32 public constant ADMIN = keccak256("ADMIN");
bytes32 public constant WRITER = keccak256("WRITER");
string public data;
constructor() {
roles[ADMIN][msg.sender] = true;
roles[WRITER][msg.sender] = true;
}
function grantRole(bytes32 role, address account) external {
require(account != address(0), "zero account");
require(roles[role][msg.sender], "only role-holder");
roles[role][account] = true;
}
function revokeRole(bytes32 role, address account) external {
require(roles[role][msg.sender], "only role-holder");
roles[role][account] = false;
}
function write(string calldata newData) external {
require(roles[WRITER][msg.sender], "not writer");
data = newData;
}
function emergencyReset() external {
require(roles[ADMIN][msg.sender], "not admin");
data = "";
}
function hasRole(bytes32 role, address account) external view returns (bool) {
return roles[role][account];
}
}solid// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AccessControlSecure {
mapping(bytes32 => mapping(address => bool)) private _roles;
bytes32 public constant ADMIN = keccak256("ADMIN");
bytes32 public constant WRITER = keccak256("WRITER");
address public immutable owner;
string public data;
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
event DataWritten(address indexed sender, string newData);
constructor() {
owner = msg.sender;
_roles[ADMIN][msg.sender] = true;
emit RoleGranted(ADMIN, msg.sender, msg.sender);
}
modifier onlyOwner() {
require(msg.sender == owner, "only owner");
_;
}
modifier onlyAdmin() {
require(_roles[ADMIN][msg.sender], "only admin");
_;
}
function grantRole(bytes32 role, address account) external {
require(account != address(0), "zero address");
if (role == ADMIN) {
require(msg.sender == owner, "only owner can grant admin");
} else {
require(_roles[ADMIN][msg.sender], "only admin can grant");
}
if (!_roles[role][account]) {
_roles[role][account] = true;
emit RoleGranted(role, account, msg.sender);
}
}
function revokeRole(bytes32 role, address account) external {
require(account != address(0), "zero address");
if (role == ADMIN) {
require(msg.sender == owner, "only owner can revoke admin");
} else {
require(_roles[ADMIN][msg.sender], "only admin can revoke");
}
if (_roles[role][account]) {
_roles[role][account] = false;
emit RoleRevoked(role, account, msg.sender);
}
}
function write(string calldata newData) external {
require(_roles[WRITER][msg.sender], "not writer");
data = newData;
emit DataWritten(msg.sender, newData);
}
function hasRole(bytes32 role, address account) external view returns (bool) {
return _roles[role][account];
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract IntegerOverflowVulnerable {
mapping(address => uint256) public balance;
uint256 public totalSupply;
function mint(address to, uint256 amount) external {
require(to != address(0), "zero address");
balance[to] += amount;
totalSupply += amount;
}
function transfer(address from, address to, uint256 amount) external {
require(to != address(0), "zero address");
// Missing check: require(balance[from] >= amount)
balance[from] -= amount; // may revert in 0.8+; would underflow silently pre-0.8
balance[to] += amount;
}
function batchAdd(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length == amounts.length, "length mismatch");
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "zero addr in batch");
balance[recipients[i]] += amounts[i];
totalSupply += amounts[i];
}
}
function balanceOf(address who) external view returns (uint256) {
return balance[who];
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract IntegerOverflowSecure {
mapping(address => uint256) public balances;
address public owner;
uint256 public totalSupply;
uint256 public constant MAX_SUPPLY = type(uint256).max; // example
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function mint(address to, uint256 amount) external onlyOwner {
require(to != address(0), "Zero address");
require(amount > 0, "Zero amount");
require(balances[to] + amount >= balances[to], "Balance overflow");
require(totalSupply + amount >= totalSupply, "Total supply overflow");
require(totalSupply + amount <= MAX_SUPPLY, "exceeds max supply");
balances[to] += amount;
totalSupply += amount;
}
function transfer(address to, uint256 amount) external {
require(to != address(0), "Zero address");
require(amount > 0, "Zero amount");
require(balances[msg.sender] >= amount, "Insufficient balance");
require(balances[to] + amount >= balances[to], "Balance overflow");
balances[msg.sender] -= amount;
balances[to] += amount;
}
function batchAdd(uint256[] calldata amounts) external {
uint256 currentBalance = balances[msg.sender];
for (uint256 i = 0; i < amounts.length; i++) {
require(currentBalance + amounts[i] >= currentBalance, "Overflow in batch");
currentBalance += amounts[i];
}
balances[msg.sender] = currentBalance;
}
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}# Deploy vulnerable contract in Remix
# Deploy attacker contract and execute attack
# Observe behavior and balances
# Deploy secure version and verify attack fails// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumer {
AggregatorV3Interface internal priceFeed;
constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}
/**
* Returns the latest price
*/
function getLatestPrice() public view returns (int256) {
(
/* uint80 roundID */,
int256 price,
/* uint startedAt */,
/* uint timeStamp */,
/* uint80 answeredInRound */
) = priceFeed.latestRoundData();
return price;
}
/**
* Returns price decimals
*/
function getDecimals() public view returns (uint8) {
return priceFeed.decimals();
}
}import `@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.solconstructor(address _priceFeed) { }priceFeed = AggregatorV3Interface(_priceFeed);function getLatestPrice() public view returns (int256) {
(
/* uint80 roundID */,
int256 price,
/* uint startedAt */,
/* uint timeStamp */,
/* uint80 answeredInRound */
) = priceFeed.latestRoundData();
return price;
}(uint80 roundId, int256 price, uint startedAt, uint timeStamp, uint80 answeredInRound)function getDecimals() public view returns (uint8) {
return priceFeed.decimals();
}(uint80 roundId, int256 answer, uint startedAt, uint timeStamp, uint80 answeredInRound)import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const PriceConsumerModule = buildModule("PriceConsumerModule", (m) => {
// Replace this with the correct feed address for your chosen pair
const feedAddress = m.getParameter(
"feedAddress",
"0xd9132c1d762D432672493F640a63B758891B449e" // Example: ETH/USD on Somnia
);
const priceConsumer = m.contract("PriceConsumer", [feedAddress]);
return { priceConsumer };
});
export default PriceConsumerModule;npx hardhat ignition deploy ./ignition/modules/Lock.js --network somnianpx create-next-app@latest somnia-protofire-example
cd somnia-protofire-examplenpm install viemimport { useEffect, useState } from 'react';
import { createPublicClient, http, parseAbi, formatUnits } from 'viem';
import { somniaTestnet } from 'viem/chains';const client = createPublicClient({
chain: somniaTestnet,
transport: http(),
});const FEEDS = { //Testnet Price Feeds
ETH: '0x604CF5063eC760A78d1C089AA55dFf29B90937f9',
BTC: '0x3dF17dbaa3BA861D03772b501ADB343B4326C676',
USDC: '0xA4a08Eb26f85A53d40E3f908B406b2a69B1A2441',
};const abi = parseAbi([
'function getLatestPrice() view returns (int256)',
'function getDecimals() view returns (uint8)',
]);export default function PriceWidget() {
const [price, setPrice] = useState('');
const [selectedToken, setSelectedToken] = useState<'ETH' | 'BTC' | 'USDC'>('ETH');const fetchPrice = async () => {
const contractAddress = FEEDS[selectedToken];
const [rawPrice, decimals] = await Promise.all([
client.readContract({ address: contractAddress, abi, functionName: 'getLatestPrice' }),
client.readContract({ address: contractAddress, abi, functionName: 'getDecimals' }),
]);
const normalized = formatUnits(rawPrice, decimals);
setPrice(parseFloat(normalized).toFixed(2));
};useEffect(() => {
fetchPrice();
const interval = setInterval(fetchPrice, 10000);
return () => clearInterval(interval);
}, [selectedToken]);return (
...
<p>${price}</p>
...
)import { useEffect, useState } from 'react';
import { createPublicClient, http, parseAbi, formatUnits } from 'viem';
import { somniaTestnet } from 'viem/chains';
const client = createPublicClient({
chain: somniaTestnet,
transport: http(),
});
const FEEDS = {
ETH: '0x604CF5063eC760A78d1C089AA55dFf29B90937f9',
BTC: '0x3dF17dbaa3BA861D03772b501ADB343B4326C676',
USDC: '0xA4a08Eb26f85A53d40E3f908B406b2a69B1A2441',
};
const abi = parseAbi([
'function getLatestPrice() view returns (int256)',
'function getDecimals() view returns (uint8)',
]);
export default function PriceWidget() {
const [price, setPrice] = useState('');
const [selectedToken, setSelectedToken] = useState<'ETH' | 'BTC' | 'USDC'>(
'ETH'
);
const fetchPrice = async () => {
const contractAddress = FEEDS[selectedToken];
const [rawPrice, decimals] = await Promise.all([
client.readContract({
address: contractAddress,
abi,
functionName: 'getLatestPrice',
}),
client.readContract({
address: contractAddress,
abi,
functionName: 'getDecimals',
}),
]);
const normalized = formatUnits(rawPrice, decimals);
setPrice(parseFloat(normalized).toFixed(2));
};
useEffect(() => {
fetchPrice();
const interval = setInterval(fetchPrice, 10000);
return () => clearInterval(interval);
}, [selectedToken]);
return (
<div className='min-h-screen flex items-center justify-center bg-gray-50'>
<div className='text-center p-6 border border-gray-200 rounded-lg shadow-lg bg-white max-w-sm w-full'>
<h3 className='text-2xl font-bold mb-4 text-gray-800'>
{selectedToken}/USD on Somnia
</h3>
<select
value={selectedToken}
onChange={(e) =>
setSelectedToken(e.target.value as 'ETH' | 'BTC' | 'USDC')
}
className='mb-6 px-4 py-2 border border-gray-300 rounded-md w-full text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500'
>
<option value='ETH'>ETH/USD</option>
<option value='BTC'>BTC/USD</option>
<option value='USDC'>USDC/USD</option>
</select>
<p className='text-4xl font-semibold text-blue-600'>${price}</p>
</div>
</div>
);
}Create a smart contract that reacts to events from other contracts automatically
npm i @somnia-chain/reactivity-contractspragma solidity ^0.8.20;
import { SomniaEventHandler } from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
contract MyEventHandler is SomniaEventHandler {
event ReactedToEvent(address emitter, bytes32 topic);
function _onEvent(
address emitter,
bytes32[] calldata eventTopics,
bytes calldata data
) internal override {
// Your business logic here
// Example: Emit a new event or update storage
emit ReactedToEvent(emitter, eventTopics[0]);
// Be cautious: Avoid reentrancy or infinite loops (e.g., don't emit events that trigger this handler)
}
}import { ethers } from "hardhat";
async function main() {
const Handler = await ethers.getContractFactory("MyEventHandler");
const handler = await Handler.deploy();
await handler.deployed();
console.log("Handler deployed to:", handler.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});import { SDK } from '@somnia-chain/reactivity';
import { somniaTestnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { createPublicClient, createWalletClient, http } from 'viem';
// Initialize SDK with the required clients
const sdk = new SDK({
public: createPublicClient({
chain: somniaTestnet,
transport: http()
}),
wallet: createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY),
chain: somniaTestnet,
transport: http(),
})
});
const subData = {
handlerContractAddress: '0xYourDeployedHandlerAddress',
priorityFeePerGas: parseGwei('2'),
maxFeePerGas: parseGwei('10'),
gasLimit: 2_000_000n, // Minimum recommended for state changes
isGuaranteed: true, // Retry on failure
isCoalesced: false, // One call per event
// Optional filters: eventTopics: ['0x...'], emitter: '0xTargetContract'
};
const txHash = await sdk.createSoliditySubscription(subData);
if (txHash instanceof Error) {
console.error('Creation failed:', txHash.message);
} else {
console.log('Subscription created! Tx:', txHash);
}