Somnia 백서

Somnia

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 33쪽

Context API ([**`useContext`**](https://react.dev/reference/react/useContext) hook). 3. Add a global NavBar in **`_app.js`** so it appears on every page. You will have a basic skeleton of a DApp, ready for **READ/WRITE** operations and UI components —topics we’ll cover in the subsequent articles. *** ## Pre-requisites: <a href="#pre-requisites" id="pre-requisites"></a> 1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript. 2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet](https://codex.somnia.network/get-started/connect-your-wallet). ## Create Your Next.js Project To create a NextJS project, run the command: ```bash npx create-next-app my-dapp-ui ``` Accept the prompts and change directory into the folder after the build is completed. This gives you a minimal Next.js setup with a pages folder (holding your routes), a public folder (for static assets), and config files. #### (Optional) Add Tailwind CSS If you plan to style your app with Tailwind, install and configure it now: ```bash npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` Then edit your tailwind.config.js: ```javascript module.exports = { content: [ "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], } ``` Finally, include Tailwind in styles/globals.css: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` *** ## Setting Up a React Context for Global State In many DApps, including this one, developers will manage **Wallet connection** and **State** globally so each page and component in the project can access it without repetitive code. This can be achieved by a [Context](https://react.dev/learn/passing-data-deeply-with-context) following React patterns. * Create a folder and name it **`contexts`** at the project root or inside **`pages`** directory. * Inside it, create a file called **`walletcontext.js`** add the following code to the file: <details> <summary>walletcontext.js</summary> ```javascript 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); } ``` </details> We parse three class methods from React: **`createContext`**, **`useContext`**, an&#x64;**`useState`** `createContext` is used to create a [context](https://react.dev/learn/passing-data-deeply-with-context) that components can provide or read. In this example, we assign `createContext` to the **`WalletContext`** variable and call the **`Provider`** method on each State and Function to make them available throughout the application. **`useWallet()`** is a custom hook, so any page or component can do: ```javascript const { connected, ... } = useWallet() ``` to access the global wallet state i.e. any of the **`Wallet.Provider`** methods **`connectToMetaMask()`** triggers the MetaMask connection flow. **`WalletProvider`** manages **State** and **Methods** in the application. *** ## Creating a Global NavBar in \_app.js Next.js automatically uses **`pages/_app.js`** to initialize every page. We will wrap the entire app in the **`WalletProvider`** inside `_app.js` and inject a **`NavBar`** menu that appears site-wide in the application #### Create the `_app.js` and add the code: ```javascript 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; ``` **`<WalletProvider>`** wraps the entire **`<Component />`** tree so that every page can share the same wallet state. **`<NavBar />`** is placed above **`<main>`**, so it’s visible on all pages. We give **`<main>`** a pt-16 to avoid content hiding behind a fixed navbar. ## NavBar Create a sub directory **components** and add a file called **`navbar.js`** Add the code: ```javascript 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> ); } ``` It uses **`useWallet()`** to read the global `connected` and `address`states, and the **`disconnectWallet`** function. The truncated `address` is displayed if a user is logged into the App or “Not connected” otherwise. A **Logout** button calls **`disconnectWallet()`** to reset the global state. *** ## Test Your Setup Start the dev server: ```bash npm run dev ``` Open **<http://localhost:3000>** in a Web Browser.\ \ You should see your NavBar at the top. <figure><img src="https://2122549367-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkYErT9t3BJtpPfejLO6I%2Fuploads%2Fl9fqbfW5Kr8EGo0TI4H1%2FNot_Logged_In.png?alt=media&amp;token=7ed9498d-59ea-404a-89d8-e2e196073877" alt=""><figcaption><p>Not Logged In</p></figcaption></figure> <figure><img src="https://2122549367-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkYErT9t3BJtpPfejLO6I%2Fuploads%2FyctcgnYDLdzAshYqW54b%2FLogged_In.png?alt=media&amp;token=6f8b2631-921e-444a-a11d-0e8c91dd42f1" alt=""><figcaption><p>Logged In</p></figcaption></figure> Because we haven’t built any advanced pages yet, you will see a blank home page. The important part is that your **WalletContext** and global **NavBar** are in place and ready for the next steps.\ <br> *** ### 5. Next Steps * Article 2 shows you how to implement READ/WRITE operations (e.g., deposit, create proposals, vote, etc.) across different Next.js pages—using the same WalletContext to handle contract calls. * Article 3 will focus on UI components, like forms, buttons, and event handling, tying it all together into a polished user interface. Congratulations! You have a clean foundation, a Next.js project configured with Tailwind, a global context to manage wallet states, and a NavBar appearing across all routes. This sets the stage for adding contract interactions and advanced UI flows in the subsequent articles. Happy building! <br> # DAO UI Tutorial p2 This guide will focus exclusively on implementing Read Operations, which fetches data from your deployed [DAO Smart Contract](/developer/building-dapps/example-applications/dao-smart-contract). By the end of this article, you’ll be able to: 1. Understand how to read data from your smart contract using [viem](/developer/development-frameworks/using-the-viem-library). 2. Implement functions to fetch the total number of proposals and specific proposal details. 3. Integrate these READ operations into your Next.js pages to display dynamic data. Prerequisite:\ Ensure you’ve completed [Part 1](/developer/building-dapps/example-applications/dao-ui-tutorial-p1) of this series, where you initialized a Next.js project, set up a **`WalletContext`** for global state management, and added a global **`NavBar`**. *** ## Understand READ Operations In decentralized applications (dApps), Read Operations involve fetching data from the blockchain without altering its state. In the example DAO Smart Contract, this is crucial for displaying dynamic information such as: * **Total Number of Proposals**: How many proposals have been created. * **Proposal Details**: Information about a specific proposal, including its description, votes, and execution status. These operations are read-only and do not require the user to sign any transactions, making them free of gas costs. We’ll use the viem library to interact with our smart contract and perform these READ operations. *** ## Expand walletcontext.js for Read Operations **`walletcontext.js`** is the central hub for managing wallet connections and interacting with your Smart Contract. We’ll add two primary READ functions: 1. **`fetchTotalProposals()`**: Retrieves the total number of proposals created. 2. **`fetchProposal(proposalId)`**: Fetches details of a specific proposal by its ID. ### Fetch Total Proposals Functionality: This function calls the **`totalProposals`** method in your smart contract to determine how many proposals have been created so far. <details> <summary>contexts/walletcontext.js</summary> ```javascript 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); } ``` </details> **`fetchTotalProposals()`** uses **`publicClient.readContract`** to call the **`totalProposals`** function in the Smart Contract. This function returns a `BigInt` representing the total number of proposals.\ \ \&#xNAN;**`fetchProposal(proposalId)`** calls the proposals mapping in your contract to retrieve details of a specific proposal by its `ID`. It returns a struct containing the proposal's **description**, **deadline**, **votes**, **execution status**, and **proposer**. *** ## Integrate Read Operations into Pages With the read functions in place, let’s integrate them into Next.js pages to display dynamic data. ### Home Page Update the `index.js` page to show the total number of proposals created in your DAO on the home page. <details> <summary>pages/index.js</summary> ```javascript 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> ); } ``` </details> Here we set the `totalProposals` state variable to store the fetched total number of proposals.\ The **`ConnectButton`** implements the MetaMask authentication. The useWallet hook parse the function from **`WalletContext.`** The **`useEffect`** Hook is applied on component mount, **`fetchTotalProposals()`** is then called to retrieve the total number of proposals from the Smart Contract. The page displays a loading message until totalProposals is fetched. Once fetched, it displays the total number of proposals. Users will have to click the **ConnectButton** to connect their wallets for WRITE operations. See part 3. *** ### Fetch-Proposal Page This page allow users to input a proposal ID, fetch its details, and display them. Additionally, on the page users are provided options for voting on or executing the proposal. Implementation: <details> <summary>pages/fetch-proposal.js</summary> ```javascript 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> ); } ``` </details> The following React states are implemented * `proposalId`: Stores the user-inputted proposal ID. * `proposalData`: Stores the fetched proposal details. * `error`: Captures any errors during fetch, vote, or execute operations. The **`handleSubmit`** function is used to validate the input and connection status. It then calls the **`fetchProposal(proposalId)`** to retrieve proposal details. We use a **Form** element for users to input a proposal ID and fetch its details. The **Error** Display is implemented to show any errors that occur during operations. The **Proposal Details** displays the fetched proposal information in a styled card. The card contains **Vote** and **Execute b**uttons for users to vote **YES/NO** or execute the proposal if eligible. *** ## Edge Cases and Errors For better UX, consider adding loading indicators while fetching data or awaiting transaction confirmations. Example: <details> <summary>pages/fetch-proposal.js</summary> ```javascript 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> ``` </details> *** ## Test Read Operations ### Populate Some Data Before testing read operations, make sure there are some proposals created: 1. Load your Smart Contract on the [Remix IDE](/developer/building-dapps/tokens-and-nfts/create-erc20-tokens). 2. Deposit 0.001 ETH to gain voting power. 3. Create one or more proposals via the Create Proposal page. ### Verify Read Operations Run your application using the command: ``` npm run dev ``` Your application will be running on **`localhost:3000`** in your web browser. Check for the following in the User Interface: 1. Total Proposals: On the Home page, verify that the total number of proposals matches the number you’ve created via Remix IDE. 2. Fetch Proposal Details:\ \- Navigate to the Fetch-Proposal page.\ \- Input a valid proposalId (e.g., 0 for the first proposal).\ \- Verify that all proposal details are accurately displayed. Monitor the browser console for any errors or logs that can help in debugging. *** ## Conclusion and Next Steps In Part 2, you successfully implemented Read Operations in your DAO front end: * **`fetchTotalProposals()`**: Displayed the total number of proposals on the Home page. * **fetchProposal(proposalId)**: Retrieved
프로젝트로 돌아가기