如何在 Solana Anchor 程序中使用程序派生地址
- 原文链接:www.quicknode.com/guides...
- 译者:AI翻译官,校对:翻译小组
- 本文链接:learnblockchain.cn/article…
Solana 编程的重要组成部分是程序派生地址 (PDA)。 PDA 是特定的地址,程序可以以编程方式生成有效的交易签名。这使得程序能够提供无信任服务,例如用于安全管理交易、投注或 DeFi 协议的托管账户。
Solana 根据程序定义的种子和程序 ID 确定性地派生 PDA。有关生成的程序地址的更多信息,请访问 docs.solana。
在本指南中,你将使用 Anchor 和 Solana Playground 创建一个 Solana 程序。该程序将允许任何用户(钱包)为任何餐厅创建或编辑评论。每个条目,程序将存储评论者、餐厅名称、评分和评论。
你将学习:
程序派生地址 (PDA) 是一种在 Solana 区块链上的账户类型,它与程序关联并由程序拥有,而不是特定用户或账户。 PDA 允许我们创建唯一数据关联、管理托管余额以及处理许多其他无信任应用程序。 PDA 是通过传递一组种子(例如,escrow_account、signer_id 等)和程序 ID 确定性生成的。与典型的密钥对不同,PDA 没有对应的私钥。它们是通过将种子和程序 ID 通过 sha256 哈希函数传递,寻找不在 ed25519 椭圆曲线 上的地址(在曲线上的地址是密钥对)。"bump" 实际上是我们用来确定性地查找 PDA 的一组距离曲线的距离。
在我们的程序中,我们将使用 PDA 来存储每个评论。我们将允许任何用户为任何餐厅生成评论。为此,我们需要确定性生成每个独特评论者-餐厅对提交的 PDA。下面的图示说明了这在实践中如何工作:
正如你所看到的,任何评论者可能与他们评论的每个餐厅关联有任何数量的 PDA。如果这尚不完全清楚,不必担心。练习会让这些概念更容易理解。让我们开始构建吧!
在 Solana Playground 上创建一个新的 Anchor 项目。打开 lib.rs。删除现有内容,并粘贴以下代码:
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]
mod restaurant_review {
use super::*;
pub fn post_review(ctx: Context<ReviewAccounts>, restaurant: String, review: String, rating: u8) -> Result<()> {
msg!("新餐厅评论!");
Ok(())
}
}
#[derive(Accounts)]
#[instruction(restaurant: String, review: String)]
pub struct ReviewAccounts<'info> {}
#[account]
pub struct Review {
}
如果你使用过 Solana Playground 并且已经有一个余额连接的钱包,可以跳过这一步。
由于该项目仅用于演示目的,我们可以使用一个“临时”钱包。Solana Playground 使得创建一个钱包变得简单。你应该在浏览器窗口的左下角看到一个红点“未连接”。点击它:
Solana Playground 将为你生成一个钱包(或你可以导入自己的)。如果愿意,可以将其保存以备后用,并在准备好后点击继续。一个新钱包将被初始化并连接到 Solana devnet。Solana Playground 会自动空投一些 SOL 到你的新钱包,但我们将请求额外的一点,以确保我们有足够的资金来部署我们的程序。在浏览器终端中,你可以使用 Solana CLI 命令。输入 solana airdrop 2 将 2 SOL 空投到你的钱包。你的钱包现在应已连接到 devnet,余额为 6 SOL:
你准备好了!让我们开始构建吧!
首先定义我们的结构是有帮助的。最终,我们将在一个称为 Review 的数据账户中存储每个评论。每个 Review 账户将存储四个元素:
在 lib.rs 中,找到你的 Review 的账户定义,并将其替换为:
#[account]
pub struct Review {
pub reviewer: Pubkey,
pub restaurant: String,
pub review: String,
pub rating: u8
}
每当提交或更新评论时,它都应采用这种形式。现在,让我们创建我们的账户上下文,以确保我们将我们的评论保存在正确的 PDA 中。
正如你从以前的练习中所知道的,我们的上下文对于定义我们必须传递给程序的账户至关重要。我们的程序将需要能够创建(并支付)一个新的 Review 账户,如果它尚不存在。因此,我们将需要传递系统程序、我们的签名钱包和 Review 的 PDA。
找到你的 ReviewAccounts 结构占位符,添加三个必需账户:review、signer 和 system_program:
#[derive(Accounts)]
#[instruction(restaurant: String)]
pub struct ReviewAccounts<'info> {
#[account(
init_if_needed,
payer = signer,
space = 500,
seeds = [restaurant.as_bytes().as_ref(), signer.key().as_ref()],
bump
)]
pub review: Account<'info, Review>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
让我们分解一下。
首先,我们必须传递我们的 review 账户。这将最终是存储我们评论数据的地方。我们必须在 review 账户属性中传递几个约束:
_init_ifneeded 用于在账户不存在时创建账户。
payer 签署交易的钱包将为新账户支付租金(需要使用 init 或 _init_ifneeded)。
space 新账户应该持有的字节数(需要使用 init 或 _init_ifneeded)。 注意:我们在这里随意使用 200 字节。这可能限制用户输入评论的长度。我们将在未来的指南中讨论动态空间。
seeds 和 bump 将检查传递的账户是否是由当前正在执行的程序、种子和 bump 派生的 PDA。因为我们希望任何用户能够评论任何独特餐厅(仅一次),我们将传递我们在评论的餐厅名称和签名者的密钥。我们需要将这两个对象引用为 Byte Buffer,这正是 Solana 的 find_program_address 查找 PDA 所需的。
太棒了!现在我们的结构体已定义,我们应该能够编写我们的 post_review 函数。
尽管 Anchor 在后台会执行很多魔法,我们仍然需要一个函数来设置我们新账户中的数据。为此,我们必须传入我们的客户端输入(之前步骤中定义的账户上下文、餐厅、评论和评分)。
找到你的 post_review 占位符,并用以下代码替换它:
#[program]
mod restaurant_review {
use super::*;
pub fn post_review(ctx: Context<ReviewAccounts>, restaurant: String, review: String, rating: u8) -> Result<()> {
let new_review = &mut ctx.accounts.review;
new_review.reviewer = ctx.accounts.signer.key();
new_review.restaurant = restaurant;
new_review.review = review;
new_review.rating = rating;
msg!("Restaurant review for {} - {} stars", new_review.restaurant, new_review.rating);
msg!("Review: {}", new_review.review);
Ok(())
}
}
首先,我们需要让我们的程序知道传入的 PDA 账户是可变的(&_
允许我们通过引用访问账户,而 mut
使账户可变),我们将其定义为 new_review。然后对于 Review 结构体的每个元素,我们将 new_review 的值设置为客户端传入的参数(并将 reviewer 设置为 signer)。
最后,我们将评论记录到 Solana 消息日志中。
在部署生产应用程序时,你将希望在指令中包含一些错误处理和保护,但这对于我们今天的目的来说应该可以。我们将在未来的指南中讨论错误处理。
在测试我们的程序之前,我们需要编译并将其部署到 Devnet:
通过点击左上角的文档图标 📑 返回到你的 Solana Playground 浏览器,并导航到 client.ts。我们将编写一个简单的脚本来调用我们的程序。
// Step 1 - Define Review Inputs
const RESTAURANT = "Quick Eats";
const RATING = 5;
const REVIEW = "Always super fast!";
// Step 2 - Fetch the PDA of our Review account
const [REVIEW_PDA] = await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from(RESTAURANT), pg.wallet.publicKey.toBuffer()],
pg.program.programId
);
console.log(`Reviewer: ${pg.wallet.publicKey.toString()}`);
console.log(`Review PDA: ${REVIEW_PDA.toString()}`);
// Step 3 - Fetch Latest Blockhash
let latestBlockhash = await pg.connection.getLatestBlockhash('finalized');
// Step 4 - Send and Confirm the Transaction
const tx = await pg.program.methods
.postReview(
RESTAURANT,
REVIEW,
RATING
)
.accounts({ review: REVIEW_PDA })
.transaction();
const txId = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair]);
console.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`);
// Step 5 - Fetch the data account and log results
const data = await pg.program.account.review.fetch(REVIEW_PDA);
console.log(`Reviewer: `,data.reviewer.toString());
console.log(`Restaurant: `,data.restaurant);
console.log(`Review: `,data.review);
console.log(`Rating: `,data.rating);
现在你可以访问 RPC 端点的日志,帮助你更有效地排查问题。如果你在 RPC 调用中遇到问题,只需检查 QuickNode 仪表板中的日志,以快速识别和解决问题。在 我们的定价页面 上了解有关日志历史限制的更多信息。
让我们分解一下:
当你准备好评论时,点击 "▶️ Run"。你应该看到该程序已记录你的评论!
因为我们设置的 PDA 依赖于餐厅和签名者密钥,你可以通过修改 RESTAURANT 在客户端中添加更多条目并再次运行。如果你以相同的 RESTAURANT 名称重新运行,程序(如实现的那样)将覆盖先前的条目。
做得很好!
做得很好!理解如何创建和与 PDA 互动是开发 Solana 的最强大工具之一。想要继续练习吗?以下是几种修改此代码以继续学习的想法:
如果你卡住了、有问题,或者只想谈谈你正在构建的内容,可以在 Discord 或 Twitter 上与我们联系!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!