Tích hợp ứng dụng frontend Next.js với hợp đồng thông minh
Ở các bước trước, bạn đã xây dựng và triển khai thành công hợp đồng thông minh tới máy chủ cục bộ. Bây giờ đã đến lúc tương tác với nó từ giao diện. Phần giao diện sử dụng Next.js, tích hợp Semaphore để có tính năng bảo mật và Mini Dapp SDK để xác thực.
Thiết lập & Cài đặt
Sau khi đã sao chép kho lưu trữ dự án bao gồm cả giao diện Next.js, việc tiếp theo bạn muốn làm là tạo tệp .env trong thư mục gốc bằng cách sử dụng lệnh bên dưới:
touch .env
Lưu ý: Đảm bảo bạn đang ở thư mục gốc trước khi chạy lệnh trên.
Bên trong tệp .env bạn vừa tạo, hãy thêm nội dung sau:
SURVEY_FACTORY_V1_CONTRACT_ADDRESS=[factory address]NEXT_PUBLIC_SURVEY_FACTORY_V1_CONTRACT_ADDRESS=[factory address]NODE_URL=http://localhost:8545
Đảm bảo thay thế SURVEY_FACTORY_V1_CONTRACT_ADDRESS
và NEXT_PUBLIC_SURVEY_FACTORY_V1_CONTRACT_ADDRESS
bằng địa chỉ hợp đồng mà bạn đã triển khai tới máy chủ cục bộ trước đó trong hướng dẫn này.
Bây giờ chúng ta hãy xem xét các chức năng cốt lõi của ứng dụng:
Quản lý khảo sát
1. Tạo Khảo sát
Định nghĩa giao diện:
Giao diện SurveyInfo chuẩn hóa cách xử lý dữ liệu khảo sát trên toàn bộ ứng dụng, đảm bảo tính nhất quán về kiểu dữ liệu và cấu trúc.
// types/index.tsexport interface SurveyInfo { title: string; // Survey's display title desc: string; // Detailed survey description id: string; // Unique identifier (contract address) remaining: number; // Number of responses still needed reward: string; // Per-response reward amount respondents: number; // Current number of participants daysleft: number; // Time until expiration finished: boolean; // Survey completion status}
Triển khai và sử dụng
MỘT. Lấy dữ liệu: Lớp này quản lý việc giao tiếp giữa các hợp đồng thông minh và ứng dụng, chuyển đổi dữ liệu blockchain thô sang định dạng SurveyInfo.
// backend/survey.tsxexport const getSurvey = async (address: string): Promise<SurveyInfo> => { const survey = getSurveyV1(address); const info = await survey.surveyInfo(); return decodeSurveyInfo(info, address);};// Decode contract data to SurveyInfoconst decodeSurveyInfo = (info: any[], contractAddr: string): SurveyInfo => { return { title: info[0], desc: info[1], id: contractAddr, remaining: remainedSurvey(info[4], info[5]), reward: ethers.formatEther(info[3]), respondents: info[5], daysleft: daysLeft(info[7]), finished: info[10] };};// Helper functionsconst remainedSurvey = (targetNumber: number, respondents: number) => { return targetNumber - respondents;};const daysLeft = (duration: bigint) => { const now = BigInt(Math.floor(Date.now() / 1000)); return Number(duration - now) / 86400; // Convert to days};
B. Hiển thị các thành phần: Thành phần React này xử lý việc thể hiện trực quan dữ liệu khảo sát, bao gồm các chỉ báo trạng thái, theo dõi tiến trình và các thành phần tương tác khác.
// components/SurveyCard.tsxinterface SurveyCardProps extends SurveyInfo {}export default function SurveyCard({ id, title, desc, reward, remaining, respondents, daysleft, finished,}: SurveyCardProps) { return ( <div className="flex flex-col rounded-lg bg-violet-400"> {/* Status Badge */} {finished ? ( <div className="bg-red-400 rounded-2xl"> <span>Finished</span> </div> ) : ( <div className="bg-lime-400 rounded-2xl"> <span>In Progress</span> </div> )} {/* Time Remaining */} {!finished && ( daysleft < 1 ? ( <div className="bg-amber-600 rounded-2xl"> <span>{`h-${Math.floor(daysleft * 24)}`}</span> </div> ) : ( <div className="bg-teal-500 rounded-2xl"> <span>D-{Math.floor(daysleft)}</span> </div> ) )} {/* Reward Display */} <span className="text-white"> <span className="font-bold">{reward}</span> KAIA </span> {/* Survey Details */} <div className="px-5 pt-2"> <h1 className="text-xl font-bold"> {title.length > 20 ? title.substring(0, 20) + "..." : title} </h1> <p className="text-white mt-1 break-words"> {desc.length > 65 ? desc.substring(0, 65) + "..." : desc} </p> </div> {/* Statistics */} <div className="flex justify-start"> <div className="flex flex-col mr-8"> <span>Remains</span> <span className="font-bold">{remaining}</span> </div> <div className="flex flex-col"> <span>Respondents</span> <span className="font-bold">{respondents}</span> </div> </div> </div> );}
C. Danh sách khảo sát: Xử lý việc tổ chức và hiển thị các bộ sưu tập khảo sát, có các mục như Chủ đề nóng và Sắp kết thúc để cải thiện khả năng điều hướng của người dùng.
// app/[locale]/square/surveys/page.tsxexport default async function SurveysPage() { const data = await getAllSurveyV1s(); // Sort and filter surveys const hotTopics = data .sort((a, b) => b.respondents - a.respondents) .slice(0, 10); const endingSoon = data .filter((survey) => survey.daysleft < 2); return ( <div className="flex flex-col mt-5"> {/* Hot Topics Section */} {hotTopics.length > 0 && ( <div> <h1 className="text-3xl font-bold">Hot Topics</h1> <div className="flex overflow-x-scroll"> {hotTopics.map((survey) => ( <SurveyCard key={survey.id} {...survey} /> ))} </div> </div> )} {/* Ending Soon Section */} {endingSoon.length > 0 && ( <div> <h1 className="text-3xl font-bold">Ending Soon</h1> <div className="flex overflow-x-scroll"> {endingSoon.map((survey) => ( <SurveyCard key={survey.id} {...survey} /> ))} </div> </div> )} </div> );}
2. Trả lời khảo sát
MỘT. Định nghĩa giao diện: Giao diện Trả lời cung cấp định dạng chuẩn cho các phản hồi khảo sát, bao gồm cả dữ liệu câu trả lời và thông tin bằng chứng liên quan đến quyền riêng tư.
// types/index.tsinterface Answer { respondent: string; // Wallet address of respondent answers: number[]; // Array of selected answer indices merkleTreeDepth: number; // Depth of merkle tree for proof merkleTreeRoot: string; // Root of merkle tree nullifier: string; // Unique identifier to prevent double submission points: number[]; // Proof points for zero-knowledge verification}
B. Chức năng gửi: Chức năng này quản lý tương tác với hợp đồng thông minh, bao gồm tạo và xác nhận giao dịch.
// browser/survey.tsxexport const submitAnswer = async ( surveyAddress: string, provider: Web3Provider, answer: Answer) => { const signer = await provider.getSigner(0);// Get contract instance const survey = getSurveyV1(surveyAddress, signer); try {// Submit transaction const tx = await survey.submitAnswer(answer, { gasLimit: 5000000 });// Wait for confirmation const receipt = await tx.wait(); return receipt; } catch (e: any) { console.log("error", e); return e.message; }};
C. Hiển thị các thành phần: Thành phần giao diện quản lý việc thu thập dữ liệu biểu mẫu, xác thực thông tin đầu vào của người dùng, tương tác với các nhà cung cấp Web3 và xử lý quy trình gửi dữ liệu trong khi cung cấp phản hồi phù hợp cho người dùng.
// Component for submitting answersexport default function SubmitAnswerForm({ id, questions,}: { id: string; questions: Question[];}) { const { provider, identity, account } = useWeb3(); const router = useRouter(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { // 1. Validate prerequisites if (!provider || !account) { alert("Please connect wallet first!"); return; } if (!identity) { alert("You need to login with LINE first!"); return; } // 2. Collect form data const formData = new FormData(e.target as HTMLFormElement); const answers = Array.from(formData.values()).map(val => parseInt(val as string) ); // 3. Generate proof const group = new Group(members); const proof = await generateProof( identity, group, new Uint8Array(answers), groupId ); // 4. Prepare answer object const answer = { respondent: account, answers, merkleTreeDepth: proof.merkleTreeDepth, merkleTreeRoot: proof.merkleTreeRoot, nullifier: proof.nullifier, points: proof.points }; // 5. Submit answer const receipt = await submitAnswer(id, provider, answer); // 6. Handle success if (receipt.status) { alert("Successfully submitted!"); router.push(`/square/surveys/${id}`); } } catch (error) { // Handle specific error cases if (error.code === 'INSUFFICIENT_FUNDS') { alert("Insufficient funds for transaction"); } else if (error.code === 'ALREADY_SUBMITTED') { alert("You have already submitted an answer"); } else { alert("Failed to submit: " + error.message); } } }; return ( <form onSubmit={handleSubmit}> {/* Form fields */} </form> );}