跳至主要内容
本页面使用机器翻译自英语,可能包含错误或不清楚的语言。如需最准确的信息,请参阅英文原文。由于更新频繁,部分内容可能与英文原文有出入。请加入我们在 Crowdin 上的努力,帮助我们改进本页面的翻译。 (Crowdin translation page, Contributing guide)

将 Next.js 前端应用程序与智能合约集成

在前面的步骤中,您已成功构建智能合约并将其部署到本地主机。 现在,是时候从前端与它互动了。 前端使用 Next.js,集成了用于隐私功能的 Semaphore 和用于身份验证的 Mini Dapp SDK。

设置和安装

克隆了包含 Next.js 前端的项目 repo 后,接下来要做的就是使用下面的命令在根目录下创建一个 .env 文件:


touch .env

注意:运行上述命令前,请确保您已进入根目录。

在刚刚创建的 .env 文件中,添加以下内容:


SURVEY_FACTORY_V1_CONTRACT_ADDRESS=[factory address]
NEXT_PUBLIC_SURVEY_FACTORY_V1_CONTRACT_ADDRESS=[factory address]
NODE_URL=http://localhost:8545

备注

确保将 SURVEY_FACTORY_V1_CONTRACT_ADDRESSNEXT_PUBLIC_SURVEY_FACTORY_V1_CONTRACT_ADDRESS 替换为本教程前面部署到 localhost 的合同地址。

现在,让我们来看看该程序的核心功能:

调查管理

1. 创建调查

接口定义:

SurveyInfo 接口规范了整个应用程序处理调查数据的方式,确保了数据类型和结构的一致性。


// types/index.ts
export 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
}

实施和使用

A. 数据获取:该层管理智能合约与应用程序之间的通信,将原始区块链数据转换为 SurveyInfo 格式。


// backend/survey.tsx
export const getSurvey = async (address: string): Promise<SurveyInfo> => {
const survey = getSurveyV1(address);
const info = await survey.surveyInfo();
return decodeSurveyInfo(info, address);
};
// Decode contract data to SurveyInfo
const 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 functions
const 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. 显示组件:此 React 组件处理调查数据的可视化表示,包括状态指示器、进度跟踪和其他交互式元素。


// components/SurveyCard.tsx
interface 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. 调查列表:处理调查表集合的组织和显示,设有_热门话题_和_即将结束_等部分,以改进用户导航。


// app/[locale]/square/surveys/page.tsx
export 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. 回答调查问卷

A. 接口定义:回答接口为调查回答提供标准格式,包括回答数据和与隐私相关的证明信息。


// types/index.ts
interface 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. 提交功能: 该功能管理与智能合约的交互,包括交易创建和确认。


// browser/survey.tsx
export 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. 显示组件:前端组件管理表单数据收集、验证用户输入、与 Web3 提供商交互、处理提交流程,同时提供适当的用户反馈。


// Component for submitting answers
export 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>
);
}

认证与社交功能集成

线路 LIFF 验证

提供安全的用户身份验证和访问 LINE 用户配置文件,同时确保应用程序在 LINE 环境中正常运行。

LIFF 初始化


// context/LiffProvider.tsx
export const LiffProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [liffObject, setLiffObject] = useState<typeof liff | null>(null);
const [liffError, setLiffError] = useState(null);
useEffect(() => {
// Check if running in LINE environment
if (liff.isInClient()) {
liff
.init({
liffId: process.env.NEXT_PUBLIC_LIFF_ID as string
})
.then(() => {
console.log("LIFF initialization succeeded");
setLiffObject(liff);
})
.catch((error: any) => {
console.error("LIFF initialization failed:", error);
setLiffError(error.toString());
});
}
}, []);
return (
<LiffContext.Provider value={{ liffObject, liffError }}>
{children}
</LiffContext.Provider>
);
};

登录实施


// components/buttons/LineLoginBtn.tsx
export default function LineLoginBtn() {
const { liffObject } = useLiff();
const loginRequest = async () => {
if (!liffObject) {
return;
}
const login = await liffObject.login();
if (!login) {
return;
}
};
return (
<button
onClick={() => {
loginRequest();
}}
>
LINE Login
</button>
);
}

Web3 集成

处理钱包连接、账户管理和区块链交互,同时维护整个应用程序的状态。

Web3 提供商设置


// context/Web3Provider.tsx
export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [provider, setProvider] = useState<Web3Provider | null>(null);
const [account, setAccount] = useState<string | null>(null);
const [isConnected, setIsConnected] = useState<boolean>(false);
const { liffObject, dappPortalSDK } = useLiff();
// Initialize from session storage
useEffect(() => {
const storedAccount = sessionStorage.getItem(WALLET_ACCOUNT_KEY);
const storedIsConnected = sessionStorage.getItem(WALLET_IS_CONNECTED_KEY);
if (storedAccount) {
setAccount(storedAccount);
}
if (storedIsConnected) {
setIsConnected(true);
}
}, []);
// Persist state changes
useEffect(() => {
if (account) {
sessionStorage.setItem(WALLET_ACCOUNT_KEY, account);
} else {
sessionStorage.removeItem(WALLET_ACCOUNT_KEY);
}
}, [account]);
return (
<Web3Context.Provider value={{ provider, account, isConnected }}>
{children}
</Web3Context.Provider>
);
};

钱包连接


const connectWallet = async () => {
try {
// 1. Get wallet provider from Mini Dapp SDK
const provider = dappPortalSDK?.getWalletProvider();
// 2. Create Web3 provider instance
const web3Provider = new w3(provider);
// 3. Request account access
const accounts = await web3Provider.send("kaia_requestAccounts", []);
// 4. Get payment provider for transactions
const pProvider = dappPortalSDK?.getPaymentProvider();
setPProvider(pProvider as PaymentProvider);
// 5. Create identity if necessary
if (
provider &&
liffObject &&
(provider.getWalletType() === WalletType.Liff ||
provider.getWalletType() === WalletType.Web) &&
liffObject.isLoggedIn()
) {
const identity = await createIdentity(
web3Provider,
accounts[0],
liffObject
);
setIdentity(identity);
}
// 6. Update state
setProvider(web3Provider);
setAccount(accounts[0]);
setIsConnected(true);
} catch (error) {
console.error("Wallet connection error:", error);
throw error;
}
};

扩展社交功能

朋友邀请系统

该平台结合了 LINE 的社交功能,使用户能够通过无缝共享体验邀请朋友。 这是通过 LIFF ShareTargetPicker 实现的,它为好友选择提供了本地 LINE 接口。

** 提供者界面**


interface LiffContextType {
// Core authentication properties
liffObject: any;
liffError: any;
// Social feature properties
inviteFriends: () => void;
encodedUID: string | null;
loading: boolean;
}

实施朋友邀请


const inviteFriends = async () => {
// 1. Check authentication
if (liff && !liff.isLoggedIn()) {
liff.login();
return;
}
// 2. Generate invitation code
let encodedUID;
try {
const result = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/invite/encode`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
idToken: liff.getAccessToken(),
}),
}
);
encodedUID = result.encodedUID;
} catch (error) {
alert("Error when encoding user ID");
return;
}
// 3. Share with friends
const msg = getFlexMessage(locale, encodedUID);
if (liff.isApiAvailable("shareTargetPicker")) {
await liff.shareTargetPicker([msg]);
}
};

转介系统

转介系统通过 URL 参数中的编码 UID 跟踪用户邀请。


// Referral check in LiffProvider
useEffect(() => {
liff.init({
liffId: process.env.NEXT_PUBLIC_LIFF_ID as string,
}).then(() => {
// Parse referral code if present
if (window.location.search !== "") {
const encodedUID = parseEncodedUID(window.location.search);
setEncodedUID(encodedUID);
// Handle referral logic
}
});
}, []);

共享消息模板

可定制的 LINE Flex 消息邀请函,支持多种语言。


const getFlexMessage = (locale: string, encodedUID: string): LiffMessage => {
const message = inviteMessages[locale] || inviteMessages["en"];
return {
type: "flex",
altText: message.altText,
contents: {
type: "bubble",
hero: {
type: "image",
url: "your-image-url",
size: "full",
aspectRatio: "20:13",
aspectMode: "cover",
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: encodedUID,
weight: "bold",
size: "xl",
},
{
type: "text",
text: message.contentsText2,
wrap: true,
},
],
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
action: {
type: "uri",
label: message.footerLabel,
uri: `https://liff.line.me/your-liff-id?encodedUID=${encodedUID}`,
},
},
],
},
},
};
};

联合提供商设置


export const LiffProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
// Authentication states
const [liffObject, setLiffObject] = useState<typeof liff | null>(null);
const [liffError, setLiffError] = useState(null);
// Social feature states
const [encodedUID, setEncodedUID] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
// Initialize everything
useEffect(() => {
initializeLIFF();
initializeDappPortal();
checkForReferral();
}, []);
return (
<LiffContext.Provider value={{
// Auth values
liffObject,
liffError,
// Social features
inviteFriends,
encodedUID,
loading
}}>
{children}
</LiffContext.Provider>
);
};

朋友邀请组件

该平台为好友邀请提供了一个专用组件,使其可以在调查界面内轻松访问。

组件实施


// components/Friends.tsx
"use client";
export default function Friends() {
const params = useParams();
const { liffObject, loading, inviteFriends } = useLiff();
const locale = params.locale as keyof typeof inviteMessages;
const messages = inviteMessages[locale] || inviteMessages.en;
// Environment and loading checks
if (loading) return <div></div>;
if (liffObject && !liffObject.isInClient()) return <div></div>;
return (
<div className="flex flex-row items-center justify-center mb-3">
<label htmlFor="inviteButton" className="text-xl font-bold">
{messages.inviteMessage}
</label>
<button
id="inviteButton"
onClick={() => inviteFriends()}
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-2 ml-2 rounded"
>
{messages.invite}
</button>
</div>
);
}

与调查页面整合


// app/[locale]/square/surveys/page.tsx
export default async function SurveysPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const data = await getAllSurveyV1s();
const { locale } = await params;
// Sort surveys for different sections
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">
{/* Friend Invitation Component */}
<Friends />
{/* Survey Sections */}
{hotTopics.length !== 0 && (
// Hot Topics section
)}
{endingSoon.length !== 0 && (
// Ending Soon section
)}
{/* All Surveys */}
<div className="flex flex-wrap gap-5 justify-center mt-5 mb-10">
{data.map((survey) => (
<SurveyCard
key={survey.id}
{...survey}
/>
))}
</div>
</div>
);
}

隐私与安全实施 Semaphore 协议

在分散式调查系统中,保护用户隐私同时确保回复的真实性至关重要。 实施 Semaphore 协议是为了解决几个关键难题:

  1. 匿名参与:用户需要证明自己有资格在不暴露身份的情况下参与调查。
  2. 防止重复提交:系统必须防止重复提交,同时保持匿名性。
  3. 答卷隐私:调查答案应保密,用户个人无法追踪。
  4. 可验证的真实性:尽管是匿名的,但必须核实答复来自授权参与者。

Semaphore 协议通过使用零知识证明来应对这些挑战,允许用户证明自己在群组中的成员身份,并在不暴露身份的情况下提交回复。 这确保了调查过程中的隐私和数据完整性。

1. 身份创建:使用多种因素生成确定性身份,确保唯一性和安全性,同时维护隐私。


//.. browser/survey.tsx
export const createIdentity = async (
web3: Web3Provider,
address: string,
liffObject: typeof liff
) => {
// 1. Get LINE user identity
const idToken = liffObject.getDecodedIDToken();
if (!idToken) {
throw Error("Failed to get ID token");
}
try {
// 2. Extract unique LINE user ID
const uid = idToken.sub;
// 3. Create deterministic message
const msg = "hello destat" + uid + address;
const hexMsg = ethers.hexlify(ethers.toUtf8Bytes(msg));
// 4. Get wallet signature as entropy source
// const secret = await web3.send("kaia_signLegacy", [address, hexMsg]);
const secret = await web3.send("personal_sign", [hexMsg, address]);
// 5. Create Semaphore identity
return new Identity(secret);
} catch (e) {
console.log("error", e);
throw Error("Failed to create identity");
}
};

2. 群组管理:在不泄露个人身份的情况下,处理群组成员操作和验证。

加入小组实施


//..backend/survey.tsx
export const joinGroup = async (
surveyAddress: string,
commitment: bigint,
signature: ethers.SignatureLike,
idToken: string,
address: string
) => {
// 1. Get group contract
const survey = getManagerConnectedSurveyV1(surveyAddress);
// 2. Verify LINE token
const profile = await isValidToken(idToken);
// 3. Verify wallet ownership
await verifyLineIdentity(profile.userId, address, signature);
// 4. Add member to group
const tx = await survey.joinGroup(commitment);
// 5. Verify successful addition
const receipt = await tx.wait();
return receipt;
};

组件结构

在本节中,我们将对组件结构进行细分。 组件文件夹应该是这样的


buttons/
├── dropdown.tsx -> Handles expandable menu navigation
├── LineLoginBtn.tsx -> Manages LINE authentication flow
└── WalletBtn.tsx -> Handles Web3 wallet connections
common/
├── index.tsx -> Exports shared components and utilities
AnswerChart.tsx -> Displays bar chart of survey responses
AnswerPie.tsx -> Shows circular visualization of responses
Footer.tsx -> Shows app footer content
Header.tsx -> Contains app header and main navigation
ItemCard.tsx -> Displays purchasable items with pricing
LinearChart.tsx -> Shows linear progress indicators
Nav.tsx -> Provides main app navigation structure
SubmitAnswerForm -> Handles survey response submission
SurveyCard.tsx -> Displays survey information and status

让这个页面变得更好