Skip to main content

将所有部分整合到一起

前端质押部分

你能感受到吗?终点线就在眼前了...至少对于这个核心部分来说是这样的。😆

我们将集中精力使程序前端的质押和解质押指令正常运行。

首先,在你的前端项目的根目录下创建一个名为 utils 的新文件夹。然后,创建一个名为 instructions.ts 的文件,并从NFT质押项目中复制/粘贴整个 instructions.ts 文件。由于代码超过200行,所以我不会在这里粘贴。😬

下一步我们将进入 StakeOptionsDisplay 文件(//components/StakeOptionsDisplay.rs)。你会注意到我们有三个空函数:handleStakehandleUnstakehandleClaim。这将是本节的重点。

和往常一样,先让我们准备好钱包和网络连接。

const walletAdapter = useWallet()
const { connection } = useConnection()

我们先确认下钱包是否已连接。

if (!walletAdapter.connected || !walletAdapter.publicKey) {
alert("Please connect your wallet")
return
}

如果一切正常,我们可以开始创建质押指示。

const stakeInstruction = createStakingInstruction(
walletAdapter.publicKey,
nftTokenAccount,
nftData.mint.address,
nftData.edition.address,
TOKEN_PROGRAM_ID, // 需要导入
METADATA_PROGRAM_ID, // 需要导入
PROGRAM_ID // 需要从constants.ts导入
)

因此,进入 utils 文件夹,添加一个名为 constants.ts 的文件,并加入以下内容:

import { PublicKey } from "@solana/web3.js"

export const PROGRAM_ID = new PublicKey(
process.env.NEXT_PUBLIC_STAKE_PROGRAM_ID ?? ""
)

这是我们在上述指示中使用的程序ID。确保你的env.local文件中有正确的程序ID

stake 指令应该准备就绪了,接下来我们要创建一笔交易,添加指令,然后发送。

const transaction = new Transaction().add(stakeInstruction)

const signature = await walletAdapter.sendTransaction(transaction, connection)

由于这是一个等待操作,确保在 handleStake 回调中添加 async 关键字。实际上,这三个函数都应该是异步回调函数。

我们可以进行检查以确认是否已完成,因此让我们获取最新的区块哈希并确认交易。

const latestBlockhash = await connection.getLatestBlockhash()

await connection.confirmTransaction(
{
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: signature,
},
"finalized"
)
} catch (error) {
console.log(error)
}

await checkStakingStatus()

确认交易后,我们可以检查是否仍在质押,因此让我们将此功能添加到 handleStake 代码块的顶部。

const checkStakingStatus = useCallback(async () => {
if (!walletAdapter.publicKey || !nftTokenAccount) {
return
}

我们还需要将 walletAdapterconnection 添加为 handleStake 回调的依赖项。

我们需要添加一些状态字段,所以向上滚动并添加质押状态的相关字段。

const [isStaking, setIsStaking] = useState(isStaked)

我们还要将参数 StakeOptionsDisplayisStaking 改为 isStaked,否则我们的状态无法正常工作。

同时,我们还需要在 utils 中创建一个名为 accounts.ts 的新文件,并从我们的NFT质押程序utils文件夹中复制文件过来。可能还需要安装我们的borsh库。

我们之所以要复制这些内容,是因为每次检查状态时,我们都要查看抵押账户的状态,并确认抵押的价值。

接下来,在 checkStakingStatus 的回调函数中,我们要调用 getStakeAccount

const account = await getStakeAccount(
connection,
walletAdapter.publicKey,
nftTokenAccount
)

setIsStaking(account.state === 0)
} catch (e) {
console.log("error:", e)
}

既然我们要发送多个交易,请继续设置一个辅助函数来确认我们的交易。我们可以将上述代码粘贴进去。

const sendAndConfirmTransaction = useCallback(
async (transaction: Transaction) => {
try {
const signature = await walletAdapter.sendTransaction(
transaction,
connection
)
const latestBlockhash = await connection.getLatestBlockhash()
await connection.confirmTransaction(
{
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: signature,
},
"finalized"
)
} catch (error) {
console.log(error)
}

await checkStakingStatus()
},
[walletAdapter, connection]
)

现在,在 handleStake 函数中只需调用 sendAndConfirmTransaction 即可。

前端索赔/兑换

现在就可以进行解除质押和领取奖励了。这两者实际上是相同的操作,不过增加了一个复杂的环节:我们是否需要为用户创建代币账户,用于存放他们即将获得的奖励代币。

下面我们将解决 handleClaim 函数。

首先,使用与之前相同的警报检查钱包适配器是否已连接并具有公钥。

接着我们要检查奖励的关联令牌账户是否存在:

const userStakeATA = await getAssociatedTokenAddress(
STAKE_MINT,
walletAdapter.publicKey
)

请快速查看我们创建的 constants.ts 文件,并为薄荷地址添加以下代码,因为我们需要 STAKE_MINT 的值:

export const STAKE_MINT = new PublicKey(
process.env.NEXT_PUBLIC_STAKE_MINT_ADDRESS ?? ""
)

当我们拥有了ATA后,我们需要调用 getAccountInfo 函数,它会返回一个账户或null

const account = await connection.getAccountInfo(userStakeATA)

随后,我们创建交易并检查是否存在一个账户,如果没有,我们调用 createAssociatedTokenAccountInstruction 函数;否则,我们调用 createRedeemInstruction 函数。

const transaction = new Transaction()

if (!account) {
transaction.add(
createAssociatedTokenAccountInstruction(
walletAdapter.publicKey,
userStakeATA,
walletAdapter.publicKey,
STAKE_MINT
)
)
}

transaction.add(
createRedeemInstruction(
walletAdapter.publicKey,
nftTokenAccount,
nftData.mint.address,
userStakeATA,
TOKEN_PROGRAM_ID,
PROGRAM_ID
)
)

现在我们可以调用上面创建的辅助事务确认函数。

await sendAndConfirmTransaction(transaction)
}, [walletAdapter, connection, nftData, nftTokenAccount])

最后,别忘了将依赖项 walletAdapterconnection 添加到回调函数中。

前端解除质押操作

对于 handleUnstake 函数,我们要确保与其他函数一样使用异步处理。你可以直接从 handleClaim 复制以下内容:

if (
!walletAdapter.connected ||
!walletAdapter.publicKey ||
!nftTokenAccount
) {
alert("请连接您的钱包")
return
}

const userStakeATA = await getAssociatedTokenAddress(
STAKE_MINT,
walletAdapter.publicKey
)

const account = await connection.getAccountInfo(userStakeATA)

const transaction = new Transaction()

if (!account) {
transaction.add(
createAssociatedTokenAccountInstruction(
walletAdapter.publicKey,
userStakeATA,
walletAdapter.publicKey,
STAKE_MINT
)
)
}

接下来,我们将向交易中添加指令,并再次调用辅助函数:

transaction.add(
createUnstakeInstruction(
walletAdapter.publicKey,
nftTokenAccount,
nftData.address,
nftData.edition.address,
STAKE_MINT,
userStakeATA,
TOKEN_PROGRAM_ID,
METADATA_PROGRAM_ID,
PROGRAM_ID
)
)

await sendAndConfirmTransaction(transaction)
}

页面编辑的股份部分

我们继续转到 stake.tsx 文件(位于 //pages/stake.tsx)并进行一些与上述内容相关的修改。

首先,根据我们之前的编辑,我们需要将 isStaking 的使用更改为 isStaked。这项修改应在 <StakeOptionsDisplay> 组件中进行。我们还需要添加一个名为 nftData 的字段,并将其赋值为 nftData,我们还需要一个状态来存储这个值。

const [nftData, setNftData] = useState<any>()`

目前,我们还没有实际的数据。我们将使用一个 useEffect 钩子,在其中调用 metaplex,并通过铸币地址找到 NFT 数据。

useEffect(() => {
const metaplex = Metaplex.make(connection).use(
walletAdapterIdentity(walletAdapter)
)

try {
metaplex
.nfts()
.findByMint({ mintAddress: mint })
.then((nft) => {
console.log("在质押页面上的 NFT 数据:", nft)
setNftData(nft)
})
} catch (e) {
console.log("获取 NFT 时发生错误:", e)
}
}, [connection, walletAdapter])

不要忘了像我们之前所做的那样,获取一个连接和钱包适配器。

现在一切准备就绪,可以进行测试了。运行 npm run dev,然后在浏览器中打开本地主机。赶快试试,点击按钮吧!🔘 ⏏️ 🆒

还需要进行一些编辑

似乎还有几个方面可能需要改进。让我们回到 StakeOptionsDisplay 文件,并在 handleStake 函数之前添加以下的 useEffect 钩子。

useEffect(() => {
checkStakingStatus()

if (nftData) {
connection
.getTokenLargestAccounts(nftData.mint.address)
.then((accounts) => setNftTokenAccount(accounts.value[0].address))
}
}, [nftData, walletAdapter, connection])

这是一个快速检查,确认我们是否有 NFT 数据,如果有的话,就为 NFT 代币账户设置值。这是一个 NFT,只有一个,所以它会是第一个地址,因此索引值为 '0'

此外,在所有三个回调函数中,我们还需要将 nftData 添加为依赖项。

最后,在 handleStake 中,在创建交易之前添加以下代码:

const [stakeAccount] = PublicKey.findProgramAddressSync(
[walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()],
PROGRAM_ID
)

const transaction = new Transaction()

const account = await connection.getAccountInfo(stakeAccount)
if (!account) {
transaction.add(
createInitializeStakeAccountInstruction(
walletAdapter.publicKey,
nftTokenAccount,
PROGRAM_ID
)
)
}

我们需要一个质押账户,也就是一个程序驱动的账户(PDA),用于在程序中存储有关你的质押状态的数据。如果我们没有这样的账户,上述代码会为我们初始化它。

终于,我们完成了核心部分 4。这最后的部分有些杂乱,为确保没有遗漏任何东西,可以将整个 StakeOptionsDisplay 文件粘贴下来进行仔细检查。

如果你想进一步改进代码或有任何其他问题,请随时提出。

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"

export const StakeOptionsDisplay = ({
nftData,
isStaked,
daysStaked,
totalEarned,
claimable,
}: {
nftData: any
isStaked: boolean
daysStaked: number
totalEarned: number
claimable: number
}) => {
const walletAdapter = useWallet()
const { connection } = useConnection()

const [isStaking, setIsStaking] = useState(isStaked)
const [nftTokenAccount, setNftTokenAccount] = useState<PublicKey>()

const checkStakingStatus = useCallback(async () => {
if (!walletAdapter.publicKey || !nftTokenAccount) {
return
}

try {
const account = await getStakeAccount(
connection,
walletAdapter.publicKey,
nftTokenAccount
)

console.log("stake account:", account)

setIsStaking(account.state === 0)
} catch (e) {
console.log("error:", e)
}
}, [walletAdapter, connection, nftTokenAccount])

useEffect(() => {
checkStakingStatus()

if (nftData) {
connection
.getTokenLargestAccounts(nftData.mint.address)
.then((accounts) => setNftTokenAccount(accounts.value[0].address))
}
}, [nftData, walletAdapter, connection])

const handleStake = useCallback(async () => {
if (
!walletAdapter.connected ||
!walletAdapter.publicKey ||
!nftTokenAccount
) {
alert("Please connect your wallet")
return
}

const [stakeAccount] = PublicKey.findProgramAddressSync(
[walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()],
PROGRAM_ID
)

const transaction = new Transaction()

const account = await connection.getAccountInfo(stakeAccount)
if (!account) {
transaction.add(
createInitializeStakeAccountInstruction(
walletAdapter.publicKey,
nftTokenAccount,
PROGRAM_ID
)
)
}

const stakeInstruction = createStakingInstruction(
walletAdapter.publicKey,
nftTokenAccount,
nftData.mint.address,
nftData.edition.address,
TOKEN_PROGRAM_ID,
METADATA_PROGRAM_ID,
PROGRAM_ID
)

transaction.add(stakeInstruction)

await sendAndConfirmTransaction(transaction)
}, [walletAdapter, connection, nftData, nftTokenAccount])

const sendAndConfirmTransaction = useCallback(
async (transaction: Transaction) => {
try {
const signature = await walletAdapter.sendTransaction(
transaction,
connection
)
const latestBlockhash = await connection.getLatestBlockhash()
await connection.confirmTransaction(
{
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: signature,
},
"finalized"
)
} catch (error) {
console.log(error)
}

await checkStakingStatus()
},
[walletAdapter, connection]
)

const handleUnstake = useCallback(async () => {
if (
!walletAdapter.connected ||
!walletAdapter.publicKey ||
!nftTokenAccount
) {
alert("Please connect your wallet")
return
}

const userStakeATA = await getAssociatedTokenAddress(
STAKE_MINT,
walletAdapter.publicKey
)

const account = await connection.getAccountInfo(userStakeATA)

const transaction = new Transaction()

if (!account) {
transaction.add(
createAssociatedTokenAccountInstruction(
walletAdapter.publicKey,
userStakeATA,
walletAdapter.publicKey,
STAKE_MINT
)
)
}

transaction.add(
createUnstakeInstruction(
walletAdapter.publicKey,
nftTokenAccount,
nftData.address,
nftData.edition.address,
STAKE_MINT,
userStakeATA,
TOKEN_PROGRAM_ID,
METADATA_PROGRAM_ID,
PROGRAM_ID
)
)

await sendAndConfirmTransaction(transaction)
}, [walletAdapter, connection, nftData, nftTokenAccount])

const handleClaim = useCallback(async () => {
if (
!walletAdapter.connected ||
!walletAdapter.publicKey ||
!nftTokenAccount
) {
alert("Please connect your wallet")
return
}

const userStakeATA = await getAssociatedTokenAddress(
STAKE_MINT,
walletAdapter.publicKey
)

const account = await connection.getAccountInfo(userStakeATA)

const transaction = new Transaction()

if (!account) {
transaction.add(
createAssociatedTokenAccountInstruction(
walletAdapter.publicKey,
userStakeATA,
walletAdapter.publicKey,
STAKE_MINT
)
)
}

transaction.add(
createRedeemInstruction(
walletAdapter.publicKey,
nftTokenAccount,
nftData.mint.address,
userStakeATA,
TOKEN_PROGRAM_ID,
PROGRAM_ID
)
)

await sendAndConfirmTransaction(transaction)
}, [walletAdapter, connection, nftData, nftTokenAccount])

return (
<VStack
bgColor="containerBg"
borderRadius="20px"
padding="20px 40px"
spacing={5}
>
<Text
bgColor="containerBgSecondary"
padding="4px 8px"
borderRadius="20px"
color="bodyText"
as="b"
fontSize="sm"
>
{isStaking
? `STAKING ${daysStaked} DAY${daysStaked === 1 ? "" : "S"}`
: "READY TO STAKE"}
</Text>
<VStack spacing={-1}>
<Text color="white" as="b" fontSize="4xl">
{isStaking ? `${totalEarned} $BLD` : "0 $BLD"}
</Text>
<Text color="bodyText">
{isStaking ? `${claimable} $BLD earned` : "earn $BLD by staking"}
</Text>
</VStack>
<Button
onClick={isStaking ? handleClaim : handleStake}
bgColor="buttonGreen"
width="200px"
>
<Text as="b">{isStaking ? "claim $BLD" : "stake buildoor"}</Text>
</Button>
{isStaking ? <Button onClick={handleUnstake}>unstake</Button> : null}
</VStack>
)
}