Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions week-04/dev/my-dapp/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

// ============================================================
// RainbowKit 스타일 import
// ============================================================
// RainbowKit의 UI 컴포넌트가 제대로 표시되려면
// 반드시 이 스타일시트를 import해야 합니다.
import '@rainbow-me/rainbowkit/styles.css';

import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { config } from '@/config/wagmi';

// ============================================================
// React Query 클라이언트 설정
// ============================================================
// wagmi v2는 내부적으로 TanStack Query를 사용합니다.
// 이 클라이언트가 데이터 캐싱, 리페칭, 동기화를 담당합니다.
const queryClient = new QueryClient();

// ============================================================
// Root Layout
// ============================================================
// Next.js App Router의 루트 레이아웃입니다.
// 모든 페이지가 이 레이아웃을 공유합니다.
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
{/* ============================================================
Provider 순서가 중요합니다!
============================================================
WagmiProvider > QueryClientProvider > RainbowKitProvider
1. WagmiProvider: 가장 바깥. wagmi config를 전체 앱에 제공
2. QueryClientProvider: 데이터 페칭 상태 관리
3. RainbowKitProvider: 지갑 UI 및 연결 상태 관리
순서가 바뀌면 "Cannot find WagmiContext" 같은 오류 발생!
============================================================ */}
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
</body>
</html>
);
}
34 changes: 34 additions & 0 deletions week-04/dev/my-dapp/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { WalletConnect } from '@/components/WalletConnect';
import { ContractReader } from '@/components/ContractReader';
import { ContractWriter } from '@/components/ContractWriter';
import { EventListener } from '@/components/EventListener';
// ============================================================
// 메인 페이지
// ============================================================
// 이 페이지는 서버 컴포넌트입니다.
// 클라이언트 전용 기능(지갑 연결 등)은 WalletConnect 컴포넌트에서 처리합니다.
export default function Home() {
const contractAddress="0x4941Cd5D052b06c356aB0BFf90635D6A613E33C5"
return (
<main className="min-h-screen p-8">
<h1 className="text-2xl font-bold mb-4">Bay-17th dApp</h1>

{/* 지갑 연결 컴포넌트 */}
<WalletConnect />
<ContractReader contractAddress={contractAddress} />
<ContractWriter contractAddress={contractAddress}/>
<EventListener contractAddress={contractAddress}/>
{/* ============================================================
TODO: 여기에 컨트랙트 상호작용 컴포넌트를 추가하세요
============================================================

예시:
- <ContractReader /> : 컨트랙트 상태 읽기
- <ContractWriter /> : 컨트랙트 함수 호출
- <EventListener /> : 이벤트 구독 및 표시

참고: eth-materials/week-04/dev/wagmi-basics.md
============================================================ */}
</main>
);
}
36 changes: 36 additions & 0 deletions week-04/dev/my-dapp/components/ContractReader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'
import { useReadContract } from 'wagmi'

const counterAbi = [
{
type: 'function',
name: 'getCount',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
] as const;

type Props = {
contractAddress:`0x${string}`;
}

export function ContractReader({contractAddress}:Props){
const {data , isLoading , error} = useReadContract({
address:contractAddress,
abi:counterAbi,
functionName:'getCount',
query:{
refetchInterval:2_000,
}
});
if (isLoading) return <div>로딩중....</div>
if (error) return <div>에러 발생: {error.message}</div>

return (
<div className="mt-6 p-4 border rounded">
<h2 className='font-semibold mb-2'>Counter</h2>
<p>Count: {data?.toString()}</p>
</div>
)
}
56 changes: 56 additions & 0 deletions week-04/dev/my-dapp/components/ContractWriter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client'
import { getContractAddress } from 'viem';
import { useWriteContract , useWaitForTransactionReceipt } from 'wagmi';

const counterAbi = [
{
type: 'function',
name: 'increment',
inputs: [],
stateMutability:'nonpayable',
outputs:[]
}, {
type: 'function',
name: 'decrement',
inputs: [],
stateMutability:'nonpayable',
outputs:[]
}, {
type: 'function',
name: 'reset',
inputs: [],
stateMutability:'nonpayable',
outputs:[]
}
] as const;
type Props = {
contractAddress:`0x${string}`;
}

export function ContractWriter({contractAddress}:Props){
const {data:hash,isPending,error,writeContract} = useWriteContract();
const {isLoading:isConfirming,isSuccess} = useWaitForTransactionReceipt({hash});
const handleWrite = (functionName: 'increment'|'decrement'|'reset') =>{
writeContract({
address:contractAddress,
abi:counterAbi,
functionName,
});
}
return (
<div className='mt-6 p-4 border rounded space-y-3'>
<h2 className='font-semibold'>Counter Controls</h2>
<div className='flex gap-2'>
<button onClick={()=>handleWrite('increment')} disabled={isPending} className = "px-3 py-1 border rounded">Increasement</button>
<button onClick={()=>handleWrite('decrement')} disabled={isPending} className = "px-3 py-1 border rounded">Decreasement</button>
<button onClick={()=>handleWrite('reset')} disabled={isPending} className = "px-3 py-1 border rounded">Reset</button>
</div>
{isPending && <div>지갑 서명 대기 중 ...</div>}
{isConfirming && <div>트랜잭션 확정 대기 중...</div>}
{isSuccess && <div>처리 성공</div>}
{error && <div>에러 발생</div>}
{hash && (<div className="text-sm break-all">Tx Hash:{hash}</div>)}
</div>
)

}
88 changes: 88 additions & 0 deletions week-04/dev/my-dapp/components/EventListener.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use client'
import { usePublicClient } from 'wagmi'
import { useState, useEffect, useRef } from 'react'

const counterAbi = [
{ type:'event',
name:'CountChanged',
inputs:[{name:'newCount',type:'uint256',indexed:false}]
}
] as const

type Props = {
contractAddress:`0x${string}`;
}

export function EventListener({contractAddress}:Props){
const [events,setEvents] = useState<string[]>([]);
const publicClient = usePublicClient();
const lastBlockRef = useRef<bigint>();

useEffect(()=>{
if(!publicClient) return;

// 과거 이벤트 로드
const loadPastEvents = async ()=>{
try {
const currentBlock = await publicClient.getBlockNumber();
const fromBlock = currentBlock > 9n ? currentBlock - 9n : 0n;
const logs = await publicClient.getContractEvents({
address: contractAddress,
abi: counterAbi,
eventName:'CountChanged',
fromBlock,
toBlock: currentBlock,
});
if(logs.length > 0){
const pastEvents = logs.map((log)=>`Count -> ${log.args.newCount}`).reverse();
setEvents(pastEvents);
}
lastBlockRef.current = currentBlock;
} catch(e){
console.error('Past events error:', e);
}
};
loadPastEvents();

const poll = async ()=>{
try {
const currentBlock = await publicClient.getBlockNumber();
if(!lastBlockRef.current){
lastBlockRef.current = currentBlock;
return;
}
if(currentBlock <= lastBlockRef.current) return;

const logs = await publicClient.getContractEvents({
address: contractAddress,
abi: counterAbi,
eventName:'CountChanged',
fromBlock: lastBlockRef.current + 1n,
toBlock: currentBlock,
});

if(logs.length > 0){
const newEvents = logs.map((log)=>`Count -> ${log.args.newCount}`);
setEvents((prev)=>[...newEvents,...prev]);
}
lastBlockRef.current = currentBlock;
} catch(e){
console.error('Poll error:', e);
}
};

const interval = setInterval(poll, 4_000);
return ()=> clearInterval(interval);
},[publicClient, contractAddress]);

return(
<div className='mt-6 p-4 border rounded'>
<h2 className="font-semibold mb-2">Event Logs</h2>
{events.length===0?(
<p>이벤트 대기중</p>
):(<ul className='text-sm space-y-1'>
{events.map((e,i)=><li key={i}>{e}</li>)}
</ul>)}
</div>
)
}
90 changes: 90 additions & 0 deletions week-04/dev/my-dapp/components/WalletConnect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';

// ============================================================
// WalletConnect 컴포넌트
// ============================================================
// RainbowKit의 ConnectButton과 wagmi의 useAccount를 활용하여
// 지갑 연결 UI를 제공합니다.

import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useAccount, useBalance } from 'wagmi';

export function WalletConnect() {
// ============================================================
// useAccount Hook
// ============================================================
// 연결된 지갑의 정보를 가져옵니다.
// - address: 지갑 주소 (0x...)
// - isConnected: 연결 상태 (boolean)
// - isConnecting: 연결 중 상태 (boolean)
// - isDisconnected: 연결 해제 상태 (boolean)
const { address, isConnected } = useAccount();

// ============================================================
// useBalance Hook
// ============================================================
// 지갑의 ETH 잔액을 조회합니다.
// - data: { formatted, symbol, decimals, value }
// - isLoading: 로딩 상태
// - isError: 에러 상태
//
// enabled 옵션: isConnected가 true일 때만 쿼리 실행
const { data: balance, isLoading: isBalanceLoading } = useBalance({
address: address,
query: {
enabled: isConnected,
},
});

return (
<div className="flex flex-col gap-4">
{/* ============================================================
RainbowKit의 ConnectButton
============================================================
지갑 연결 UI를 자동으로 제공합니다:
- 연결되지 않음: "Connect Wallet" 버튼
- 연결됨: 주소, 잔액, 네트워크 표시 + 드롭다운 메뉴

커스터마이징:
- showBalance={false} : 잔액 숨기기
- chainStatus="icon" : 체인을 아이콘으로만 표시
- accountStatus="avatar" : 주소를 아바타로만 표시
============================================================ */}
<ConnectButton />

{/* ============================================================
연결된 지갑 정보 표시
============================================================
isConnected가 true일 때만 렌더링됩니다.
이 섹션을 커스터마이징하여 원하는 정보를 표시하세요.
============================================================ */}
{isConnected && (
<div className="p-4 bg-gray-100 rounded space-y-2">
<p className="font-medium">연결된 지갑</p>

{/* 지갑 주소 */}
<p className="text-sm text-gray-600">
주소: {address?.slice(0, 6)}...{address?.slice(-4)}
</p>

{/* ETH 잔액 */}
<p className="text-sm text-gray-600">
잔액: {isBalanceLoading
? '로딩 중...'
: `${balance?.formatted ?? '0'} ${balance?.symbol ?? 'ETH'}`
}
</p>

{/* ============================================================
TODO: 추가 기능 구현
============================================================
- 컨트랙트 상태 읽기 (useReadContract)
- 컨트랙트 함수 호출 (useWriteContract)
- 트랜잭션 히스토리 표시
- 토큰 잔액 표시 (ERC20)
============================================================ */}
</div>
)}
</div>
);
}
Loading