bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 28쪽
subgraphs. Click the create button and enter the required details.
<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdbWSqj7ddwv0yFj8FlHdDGGlJmeqmRakTRUNX2LUCUHlqPjcK2eSBdRwnwwZ5pq8T-3op10MJwdknOn-KfCDbsJ4nuI7Uu39obkcb8gYfVTlRhrt1s4IJ3U7VNgEnMW0mMN7hK6w?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>
<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXex3AIr44-pbEiLznhnQE0xhOYHgtOmwRyu8HwXjFWsTQLM9JtyFD10YPZdDxY0qFNVC2JUqt_29q8VtSmV0emS8VVOZrF4VfYUUxmqQM3yF-_c29E9vQaRfvVoV7Guu6AkI8mgUw?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>
After initialising the subgraph on <https://somnia.chain.love/> the next step is to create and deploy the subgraph via the terminal.
## Initialize the Subgraph
```bash
graph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytoken
```
<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeFtlgKbMC4sCBfwGl1xI76iHiOxsc7jKZtg9BUeMGiTsormp5HRYnreVlo4cpuZe23eESQT9pgbCdf9dQ4lKw_-fjsBwPLd-DZd-o_Gc3BA9VxjE95i05yuXDacPmNBbwsEvvvbA?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>
Then, update networks.json to use Somnia’s RPC
```bash
{
"somnia-testnet": {
"network": "Somnia Testnet",
"rpc": "https://dream-rpc.somnia.network",
"startBlock": 12345678
}
}
```
## Define the Subgraph Schema
📁 Edit schema.graphql
```bash
type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes!
to: Bytes!
value: BigInt!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
```
## Build the Subgraph
```bash
graph codegen
graph build
```
## Deploy the Subgraph
```bash
graph 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
```
<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfc-Mu-wBk_mvxbW4Rz6ODZK_RtWPtTxDjKcldwtudapplfGjxVMZTGS_xzX73VOp_XcRCiicQsA-q7yXG0x8Tz6vyCeTU1Lw08Kmi8rbZlWapYgskmfWXgHo3t2Rgypy3x3cfd0A?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>
## Query the Subgraph
Once your subgraph is deployed and indexing blockchain data on Somnia, you can retrieve information using GraphQL queries. These queries allow you to efficiently access structured data such as token transfers, approvals, and contract interactions without having to scan the blockchain manually.
Developers can query indexed blockchain data in real time using the Graph Explorer or a GraphQL client. This enables DApps, analytics dashboards, and automated systems to interact more efficiently with blockchain events.
This section demonstrates how to write and execute GraphQL queries to fetch blockchain data indexed by the subgraph. Go to <https://somnia.chain.love/graph/17>
### Fetch Latest Transfers
```json
{
transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
id
from
to
value
blockTimestamp
transactionHash
}
}
```
### Get Transfers by Address
```json
{
transfers(where: { from: "0xUserWalletAddress" }) {
id
from
to
value
}
}
```
### Get Transfers in a Time Range
```
{
transfers(where: { blockTimestamp_gte: "1700000000", blockTimestamp_lte: "1710000000" }) {
id
from
to
value
}
}
```
## Conclusion
This tutorial provides a complete pipeline for indexing blockchain data on Somnia using The Graph! 🔥
<br>
# Building Subgraph UIs (NextJS/Fetch)
[Subgraphs](https://somnia.chain.love) allow developers to efficiently query Somnia blockchain data using GraphQL, making it easy to index and retrieve real-time blockchain activity. In this tutorial, you’ll learn how to:
* Fetch blockchain data from a Subgraph API
* Fix CORS errors using a NextJS API route
* Display token transfers in a real-time UI
By the end of this guide, you'll have a fully functional UI that fetches and displays token transfers from Somnia’s Subgraph API.
## Prerequisites
* Basic knowledge of React & Next.js.
* A deployed Subgraph API on Somnia ([or use an existing one](https://somnia.chain.love/graph)).
* Account on <https://somnia.chain.love> see [guide](/developer/building-dapps/data-indexing-and-querying/protofire-subgraph).
## Create a NextJS Project
Start by creating a new Next.js app.
```bash
npx create-next-app@latest somnia-subgraph-ui
cd somnia-subgraph-ui
```
Then, install required dependencies.
```bash
npm install thirdweb react-query graphql
```
## Define the Subgraph API in Environment Variables
Create a .env.local file in the root folder.
```
NEXT_PUBLIC_SUBGRAPH_URL=https://proxy.somnia.chain.love/subgraphs/name/somnia-testnet/test-mytoken
NEXT_PUBLIC_SUBGRAPH_CLIENT_ID=YOUR_CLIENT_ID
```
{% hint style="info" %}
💡 Note: Restart your development server after modifying .env.local:
{% endhint %}
```bash
npm run dev
```
## Create a NextJS API Route for the Subgraph
Since the Somnia Subgraph API has CORS restrictions, we’ll use a NextJS API route to act as a proxy.
Inside the app directory create the folder paths **`api/proxy`** and add a file **`route.ts`** Update the **`route.ts`** file with the following code:
```typescript
import { 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 });
}
}
```
This code allows your frontend to make requests without triggering CORS errors.
## Fetch and Display Token Transfers in the UI
Now that we have the API set up, let’s build a React component that:
* Sends the GraphQL query using fetch()
* Stores the fetched data using useState()
* Displays the token transfers in a simple UI
To fetch the latest 10 token transfers, we’ll use this GraphQL query:
```graphql
{
transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
id
from
to
value
blockTimestamp
transactionHash
}
}
```
This code fetches the last 10 transfers (first: 10) and orders them by timestamp (latest first). It retrieves wallet addresses (from, to) and the amount transferred (value) and includes the transaction hash (used to generate an explorer link).
## Fetch and Display Data in a React Component
Now, let’s integrate the query into our NextJS frontend. Create a folder **`components`** and add a file **`TokenTransfer.ts`**
We use **`useState()`** to store transfer data and **`useEffect()`** to fetch it when the component loads.
```typescript
"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();
}, []);
```
Once data is fetched, we render it inside the UI.
```typescript
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>
);
}
```
The UI shows a loading message while data is being fetched. When data is ready, it displays the latest 10 token transfers. It formats transaction values (value / 1e18) to show the correct STT amount and provides a link to view each transaction on Somnia Explorer.
## Add the Component to the NextJS Page
Update the page.tsx file:
```typescript
"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>
);
}
```
Restart the development server:
```
npm run dev
```
<figure><img src="https://2122549367-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkYErT9t3BJtpPfejLO6I%2Fuploads%2FXbWJRdvQ4SHjCpp9uaBX%2FTokenTransfers.png?alt=media&token=cafd3254-32a7-44ed-8481-6f47afa28e59" alt=""><figcaption></figcaption></figure>
Now your NextJS UI dynamically fetches and displays token transfers from Somnia’s Subgraph! 🔥
<br>
# Building Subgraph UIs (Apollo Client)
This guide will teach you how to create a minimal, functional UI that queries blockchain data from a Somnia subgraph using Next.js, Apollo Client, and GraphQL.
## Prerequisites
* Basic knowledge of React and Next.js
* Node.js 20+
* A deployed subgraph on Somnia (we'll use [SomFlip](https://shannon-explorer.somnia.network/address/0x014F851965F281d6112FC7F6dfe8c331C413Eb9b) as an example)
## What You'll Build
A clean, minimal interface that:
* Displays all coin flip results with pagination
* Shows a live feed that auto-refreshes every 5 seconds
## Create a Next.js Project
Start by creating a new Next.js application with TypeScript and TailwindCSS:
```bash
npx create-next-app@latest somnia-subgraph-ui --typescript --tailwind --app
cd somnia-subgraph-ui
```
Install the required GraphQL dependencies:
```bash
npm install @apollo/client graphql
```
## Understand the Architecture
Before we code, let's understand how the pieces fit together:
```
User Interface (React Components)
↓
Apollo Client (GraphQL Client)
↓
GraphQL Queries
↓
Somnia Subgraph API
↓
Blockchain Data
```
## Set Up Apollo Client
Apollo Client is a comprehensive GraphQL client that manages data fetching, caching, and state management. Create a `lib` directory and create a file `apollo-client.ts`
```typescript
import { 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;
```
The `URI` is the endpoint where your subgraph is hosted, and `InMemoryCache` will store the query results in memory for fast access
## Create the Apollo Provider Wrapper
React components need access to the Apollo Client. We'll create a wrapper component that provides this access to the entire app. Create a `components` directory and create a file `ApolloWrapper.tsx`.
```typescript
'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>
);
}
```
ApolloProvider: Makes the Apollo Client available to all child components
## Update app/layout.tsx
```typescript
import ApolloWrapper from '@/components/ApolloWrapper';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ApolloWrapper>
{children}
</ApolloWrapper>
</body>
</html>
);
}
```
## Create GraphQL Queries
GraphQL queries define exactly what data you want from the subgraph. Let's create queries for our two main features. In the `lib` directory create a `queries.ts` file.
<details>
<summary>queries.ts</summary>
```
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
}
}
`;
```
</details>
Note the following:
* `gql` is the template literal tag that parses GraphQL queries
* Variables start with $ and have types (Int!, String!, etc.)
* ! means the field is required (non-nullable)
## Build the All Flips Component
Let's build the All Flips component step by step, understanding each part in detail.
### Set Up the Component File
In the `components` directory create `AllFlips.tsx` file and add the following imports.
```
'use client';
import { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_FLIP_RESULTS } from '@/lib/queries';
```
### Create Utility Functions
Add these helper functions at the top of your component:
```
/ 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();
};
```
### Component Function and State Management
```
export default function AllFlips() {
// Track which page of results we're viewing
const [page, setPage] = useState(0);
const itemsPerPage = 30;
```
`the number ofpage` tracks the current page number (starting at 0), which is used to calculate the number of results to skip. It updates when the user clicks the Previous/Next button.
### Execute the GraphQL Query
```
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
},
});
```
The `useQuery` function is set to `loading: true` while fetching `data` and the `data` contains the query results when successful.
### Handle Query States
```
// 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>;
}
```
This prevents rendering errors and andles edge cases gracefully
### Render the Table View
<details>
<summary>AllFlips.tsx - Table View</summary>
```
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>
);
}
```
</details>
## Build the Live Feed Component
Now let's build the Live Feed component that automatically refreshes to show new flips.
### Set Up the Component
In the `components` directory create a `LiveFeed.tsx` file and update the imports.
```
'use client';
import { useQuery } from '@apollo/client';
import { GET_RECENT_FLIPS } from '@/lib/queries';
```
### Create Utility Functions
```
const truncateHash = (hash: string) => {
return `${hash.slice(0, 6)}...${hash.slice(-4)}`;
};
const formatEther = (wei: string) => {
return (parseFloat(wei) / 1e18).toFixed(4);
};
```
### Component Function with Auto-Refresh
```
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)
});
```
The `pollInterval` automatically re-executes the query every 5 seconds. New flips appear without user interaction with Apollo Client handling the refresh logic. You can set to 0 or remove to disable auto-refresh
### Handle Qu프로젝트로 돌아가기