🎯 将所有部分整合到一起 - 完成NFT质押系统!
🏁 欢迎来到最后冲刺!
嘿,Web3勇士们!👋 能感受到吗?终点线就在眼前!🏃♂️💨 经过了这么长的旅程,今天我们要把所有的积木拼在一起,让你的NFT质押系统真正运转起来!
🎯 今日任务: 完成前端与智能合约的完美对接,让用户能够一键质押、领取奖励!
🗺️ 今日冒险地图
🎮 起点:准备前端环境
↓
📦 Step 1: 设置工具文件
↓
⚡ Step 2: 实现质押功能
↓
💰 Step 3: 实现领取奖励
↓
🔓 Step 4: 实现解除质押
↓
🎨 Step 5: 完善UI交互
↓
🏆 终点:完整的质押系统!
📦 Step 0: 项目准备工作
🛠️ 创建必要的文件结构
# 📁 在你的前端项目根目录创建utils文件夹
mkdir utils
cd utils
# 📝 创建核心文件
touch instructions.ts # 指令构建器
touch constants.ts # 常量定义
touch accounts.ts # 账户管理
💡 Pro Tip: 从你的NFT质押程序复制完整的
instructions.ts
文件(200+行代码)。这包含了所有与智能合约交互的指令!
📋 文件结构概览
📦 your-frontend-project/
├── 📂 pages/
│ └── 📜 stake.tsx # 质押页面
├── 📂 components/
│ └── 📜 StakeOptionsDisplay.tsx # 质押选项组件
├── 📂 utils/ # 🆕 工具文件夹
│ ├── 📜 instructions.ts # 指令构建
│ ├── 📜 constants.ts # 常量管理
│ └── 📜 accounts.ts # 账户工具
└── 📜 .env.local # 环境变量
🔧 Step 1: 设置常量和环境变量
📝 创建constants.ts
// 📁 utils/constants.ts
// 🎯 管理所有程序相关的常量
import { PublicKey } from "@solana/web3.js"
// 🏭 质押程序ID - 从环境变量读取
export const PROGRAM_ID = new PublicKey(
process.env.NEXT_PUBLIC_STAKE_PROGRAM_ID ?? ""
)
// 🪙 质押代币铸币地址 - BLD代币
export const STAKE_MINT = new PublicKey(
process.env.NEXT_PUBLIC_STAKE_MINT_ADDRESS ?? ""
)
// 💡 提示:这些地址会在.env.local中配置
console.log("📍 程序ID:", PROGRAM_ID.toString())
console.log("🪙 代币地址:", STAKE_MINT.toString())
🔐 配置.env.local
# 📁 .env.local
# ⚠️ 确保这些值与你部署的程序匹配!
# 🏭 质押程序地址
NEXT_PUBLIC_STAKE_PROGRAM_ID=你的质押程序ID
# 🪙 BLD代币地址
NEXT_PUBLIC_STAKE_MINT_ADDRESS=你的BLD代币地址
# 🌐 RPC端点(可选)
NEXT_PUBLIC_RPC_ENDPOINT=https://api.devnet.solana.com
⚡ Step 2: 实现质押功能
🎮 更新StakeOptionsDisplay组件
// 📁 components/StakeOptionsDisplay.tsx
import { VStack, Text, Button } from "@chakra-ui/react"
import { useConnection, useWallet } from "@solana/wallet-adapter-react"
import { PublicKey, Transaction } from "@solana/web3.js"
import { useCallback, useEffect, useState } from "react"
import {
createInitializeStakeAccountInstruction,
createRedeemInstruction,
createStakingInstruction,
createUnstakeInstruction,
} from "../utils/instructions"
import {
TOKEN_PROGRAM_ID,
getAssociatedTokenAddress,
createAssociatedTokenAccountInstruction,
} from "@solana/spl-token"
import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"
import { PROGRAM_ID, STAKE_MINT } from "../utils/constants"
import { getStakeAccount } from "../utils/accounts"
// 🎯 组件属性接口
interface StakeOptionsProps {
nftData: any // NFT元数据
isStaked: boolean // 初始质押状态
daysStaked: number // 质押天数
totalEarned: number // 总收益
claimable: number // 可领取数量
}
export const StakeOptionsDisplay = ({
nftData,
isStaked,
daysStaked,
totalEarned,
claimable,
}: StakeOptionsProps) => {
// 🔗 钱包和连接
const walletAdapter = useWallet()
const { connection } = useConnection()
// 📊 状态管理
const [isStaking, setIsStaking] = useState(isStaked)
const [nftTokenAccount, setNftTokenAccount] = useState<PublicKey>()
// 🔍 检查质押状态
const checkStakingStatus = useCallback(async () => {
if (!walletAdapter.publicKey || !nftTokenAccount) {
console.log("⚠️ 缺少必要信息")
return
}
try {
console.log("🔍 检查质押状态...")
// 获取质押账户信息
const account = await getStakeAccount(
connection,
walletAdapter.publicKey,
nftTokenAccount
)
console.log("📊 质押账户:", account)
// 更新状态(0 = 已质押)
setIsStaking(account.state === 0)
console.log("✅ 状态更新:", account.state === 0 ? "已质押" : "未质押")
} catch (e) {
console.error("❌ 检查状态失败:", e)
}
}, [walletAdapter, connection, nftTokenAccount])
// 🚀 初始化Effect
useEffect(() => {
console.log("🎬 组件初始化...")
checkStakingStatus()
if (nftData) {
// 获取NFT的代币账户
console.log("🔍 查找NFT代币账户...")
connection
.getTokenLargestAccounts(nftData.mint.address)
.then((accounts) => {
const tokenAccount = accounts.value[0].address
console.log("💳 NFT代币账户:", tokenAccount.toString())
setNftTokenAccount(tokenAccount)
})
}
}, [nftData, walletAdapter, connection])
// ... 继续下面的handleStake等函数
}
🎯 实现质押处理函数
// 🎯 处理质押操作
const handleStake = useCallback(async () => {
console.log("🚀 开始质押流程...")
// 🔍 Step 1: 验证钱包连接
if (!walletAdapter.connected || !walletAdapter.publicKey || !nftTokenAccount) {
alert("❌ 请先连接钱包!")
return
}
console.log("✅ 钱包已连接:", walletAdapter.publicKey.toString())
try {
// 🔑 Step 2: 派生质押账户PDA
const [stakeAccount] = PublicKey.findProgramAddressSync(
[
walletAdapter.publicKey.toBuffer(), // 用户公钥
nftTokenAccount.toBuffer() // NFT账户
],
PROGRAM_ID
)
console.log("📍 质押账户PDA:", stakeAccount.toString())
// 📦 Step 3: 创建交易
const transaction = new Transaction()
// 🔍 Step 4: 检查质押账户是否存在
const account = await connection.getAccountInfo(stakeAccount)
if (!account) {
console.log("📝 需要初始化质押账户...")
// 添加初始化指令
transaction.add(
createInitializeStakeAccountInstruction(
walletAdapter.publicKey,
nftTokenAccount,
PROGRAM_ID
)
)
} else {
console.log("✅ 质押账户已存在")
}
// ⚡ Step 5: 添加质押指令
console.log("📝 添加质押指令...")
const stakeInstruction = createStakingInstruction(
walletAdapter.publicKey, // 用户
nftTokenAccount, // NFT账户
nftData.mint.address, // NFT铸币
nftData.edition.address, // NFT版本
TOKEN_PROGRAM_ID, // Token程序
METADATA_PROGRAM_ID, // 元数据程序
PROGRAM_ID // 质押程序
)
transaction.add(stakeInstruction)
// 🚀 Step 6: 发送交易
console.log("📤 发送交易...")
await sendAndConfirmTransaction(transaction)
console.log("🎉 质押成功!")
} catch (error) {
console.error("❌ 质押失败:", error)
alert("质押失败,请重试!")
}
}, [walletAdapter, connection, nftData, nftTokenAccount])
🛠️ 创建交易辅助函数
// 🔧 发送并确认交易的辅助函数
const sendAndConfirmTransaction = useCallback(
async (transaction: Transaction) => {
try {
console.log("📤 准备发送交易...")
// 🚀 发送交易
const signature = await walletAdapter.sendTransaction(
transaction,
connection
)
console.log("📝 交易签名:", signature)
// ⏳ 获取最新区块哈希
const latestBlockhash = await connection.getLatestBlockhash()
// ✅ 确认交易
console.log("⏳ 等待确认...")
await connection.confirmTransaction(
{
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: signature,
},
"finalized" // 等待最终确认
)
console.log("✅ 交易已确认!")
// 🔄 更新状态
await checkStakingStatus()
} catch (error) {
console.error("❌ 交易失败:", error)
alert("交易失败,请重试!")
}
},
[walletAdapter, connection]
)
💰 Step 3: 实现领取奖励功能
// 💰 处理领取奖励
const handleClaim = useCallback(async () => {
console.log("💎 开始领取奖励...")
// 🔍 验证钱包
if (!walletAdapter.connected || !walletAdapter.publicKey || !nftTokenAccount) {
alert("❌ 请先连接钱包!")
return
}
try {
// 💳 Step 1: 获取用户的BLD代币账户
const userStakeATA = await getAssociatedTokenAddress(
STAKE_MINT, // BLD代币地址
walletAdapter.publicKey // 用户地址
)
console.log("💳 用户代币账户:", userStakeATA.toString())
// 🔍 Step 2: 检查账户是否存在
const account = await connection.getAccountInfo(userStakeATA)
// 📦 Step 3: 创建交易
const transaction = new Transaction()
// 🏗️ Step 4: 如果代币账户不存在,创建它
if (!account) {
console.log("📝 需要创建代币账户...")
transaction.add(
createAssociatedTokenAccountInstruction(
walletAdapter.publicKey, // 付款人
userStakeATA, // 新账户地址
walletAdapter.publicKey, // 账户所有者
STAKE_MINT // 代币类型
)
)
}
// 💎 Step 5: 添加领取奖励指令
console.log("📝 添加领取奖励指令...")
transaction.add(
createRedeemInstruction(
walletAdapter.publicKey, // 用户
nftTokenAccount, // NFT账户
nftData.mint.address, // NFT铸币
userStakeATA, // 接收代币的账户
TOKEN_PROGRAM_ID, // Token程序
PROGRAM_ID // 质押程序
)
)
// 🚀 Step 6: 发送交易
await sendAndConfirmTransaction(transaction)
console.log("🎉 成功领取奖励!")
alert(`成功领取 ${claimable} $BLD!`)
} catch (error) {
console.error("❌ 领取失败:", error)
alert("领取失败,请重试!")
}
}, [walletAdapter, connection, nftData, nftTokenAccount, claimable])
🔓 Step 4: 实现解除质押功能
// 🔓 处理解除质押
const handleUnstake = useCallback(async () => {
console.log("🔓 开始解除质押...")
// 🔍 验证钱包
if (!walletAdapter.connected || !walletAdapter.publicKey || !nftTokenAccount) {
alert("❌ 请先连接钱包!")
return
}
try {
// 💳 Step 1: 获取用户的BLD代币账户
const userStakeATA = await getAssociatedTokenAddress(
STAKE_MINT,
walletAdapter.publicKey
)
// 🔍 Step 2: 检查账户
const account = await connection.getAccountInfo(userStakeATA)
// 📦 Step 3: 创建交易
const transaction = new Transaction()
// 🏗️ Step 4: 如果需要,创建代币账户
if (!account) {
console.log("📝 创建代币账户...")
transaction.add(
createAssociatedTokenAccountInstruction(
walletAdapter.publicKey,
userStakeATA,
walletAdapter.publicKey,
STAKE_MINT
)
)
}
// 🔓 Step 5: 添加解除质押指令
console.log("📝 添加解除质押指令...")
transaction.add(
createUnstakeInstruction(
walletAdapter.publicKey, // 用户
nftTokenAccount, // NFT账户
nftData.mint.address, // NFT铸币(注意:这里用mint.address)
nftData.edition.address, // NFT版本
STAKE_MINT, // BLD代币
userStakeATA, // 接收代币账户
TOKEN_PROGRAM_ID, // Token程序
METADATA_PROGRAM_ID, // 元数据程序
PROGRAM_ID // 质押程序
)
)
// 🚀 Step 6: 发送交易
await sendAndConfirmTransaction(transaction)
console.log("🎉 解除质押成功!")
alert("成功解除质押!你的NFT已经自由了!")
} catch (error) {
console.error("❌ 解除质押失败:", error)
alert("解除质押失败,请重试!")
}
}, [walletAdapter, connection, nftData, nftTokenAccount])
🎨 Step 5: 完善页面交互
📝 更新stake.tsx页面
// 📁 pages/stake.tsx
import { useEffect, useState } from "react"
import { useConnection, useWallet } from "@solana/wallet-adapter-react"
import { Metaplex, walletAdapterIdentity } from "@metaplex-foundation/js"
import { PublicKey } from "@solana/web3.js"
import { StakeOptionsDisplay } from "../components/StakeOptionsDisplay"
const Stake = ({ mint, imageSrc }) => {
const { connection } = useConnection()
const walletAdapter = useWallet()
// 📊 NFT数据状态
const [nftData, setNftData] = useState<any>(null)
const [loading, setLoading] = useState(true)
// 🎯 获取NFT元数据
useEffect(() => {
const fetchNftData = async () => {
console.log("🔍 获取NFT数据...")
setLoading(true)
try {
// 🏗️ 创建Metaplex实例
const metaplex = Metaplex.make(connection)
.use(walletAdapterIdentity(walletAdapter))
// 🔍 查找NFT
const nft = await metaplex
.nfts()
.findByMint({ mintAddress: mint })
console.log("📦 NFT数据:", nft)
setNftData(nft)
} catch (error) {
console.error("❌ 获取NFT失败:", error)
alert("无法获取NFT信息!")
} finally {
setLoading(false)
}
}
if (connection && walletAdapter.connected) {
fetchNftData()
}
}, [connection, walletAdapter, mint])
// 🎨 渲染界面
return (
<div className="container">
{/* 🖼️ 左侧:NFT展示 */}
<div className="nft-display">
<img src={imageSrc} alt="NFT" />
{loading && <p>加载中...</p>}
</div>
{/* 🎮 右侧:质押选项 */}
{nftData && (
<StakeOptionsDisplay
nftData={nftData}
isStaked={false} // 初始状态
daysStaked={0}
totalEarned={0}
claimable={0}
/>
)}
</div>
)
}
export default Stake
💡 实用技巧与最佳实践
🎯 技巧1:错误处理
// 🛡️ 创建统一的错误处理
const handleError = (error: any, operation: string) => {
console.error(`❌ ${operation}失败:`, error)
// 解析错误类型
if (error.message?.includes("insufficient")) {
alert("余额不足!")
} else if (error.message?.includes("User rejected")) {
alert("用户取消了操作")
} else {
alert(`${operation}失败,请重试!`)
}
}
🎯 技巧2:状态管理
// 📊 使用更详细的状态管理
const [stakingState, setStakingState] = useState({
isStaking: false,
isLoading: false,
lastAction: null,
error: null
})
🎯 技巧3:用户反馈
// 🎨 添加加载状态
<Button
onClick={handleStake}
isLoading={isProcessing}
loadingText="质押中..."
disabled={!nftData}
>
质押NFT
</Button>
🧪 测试清单
✅ 测试步骤
# 1️⃣ 启动开发服务器
npm run dev
# 2️⃣ 打开浏览器
open http://localhost:3000
# 3️⃣ 测试流程
# □ 连接钱包
# □ 选择NFT
# □ 点击质押
# □ 等待一段时间
# □ 领取奖励
# □ 解除质押
🔍 调试技巧
// 🐛 在浏览器控制台查看详细日志
console.log("当前状态:", {
wallet: walletAdapter.publicKey?.toString(),
nft: nftTokenAccount?.toString(),
isStaking,
nftData
})
🚨 常见问题解决
❌ 问题1:交易失败
// 检查RPC连接
if (!connection) {
console.error("❌ 没有RPC连接")
return
}
// 检查网络
const network = await connection.getVersion()
console.log("🌐 网络版本:", network)
❌ 问题2:账户不存在
// 总是先检查账户
const account = await connection.getAccountInfo(address)
if (!account) {
console.log("📝 需要创建账户")
// 添加创建账户指令
}
🎓 知识总结
📚 核心概念回顾
┌────────────────────────────────────┐
│ 🏆 完成的功能清单 │
├────────────────────────────────────┤
│ ✅ 质押NFT到程序 │
│ ✅ 实时检查质押状态 │
│ ✅ 领取BLD代币奖励 │
│ ✅ 解除质押获取NFT │
│ ✅ 自动创建代币账户 │
│ ✅ 完整的错误处理 │
└────────────────────────────────────┘
🚀 恭喜完成!
太棒了! 🎊 你已经成功构建了一个完整的NFT质押系统!
🎯 你的成就
- 🏗️ 构建了完整的前端界面
- 🔗 实现了与智能合约的交互
- 💰 创建了代币奖励系统
- 🎮 提供了流畅的用户体验
🔮 接下来可以做什么?
- 添加更多功能 - 批量质押、排行榜
- 优化UI - 动画、实时更新
- 部署到主网 - 让真实用户使用
- 创建移动版 - 支持移动钱包
💬 记住: 这是Core 4的最后一部分,你已经掌握了构建完整DApp的所有技能!
继续探索,成为下一个Web3独角兽的创始人! 🦄🚀✨