如何在 Solana Anchor 程序中使用程序派生地址
- 原文链接:www.quicknode.com/guides...
- 译者:AI翻译官,校对:翻译小组
- 本文链接:htzkw.com …
概述
Solana 编程的重要组成部分是程序派生地址 (PDA)。 PDA 是特定的地址,程序可以以编程方式生成有效的交易签名。这使得程序能够提供无信任服务,例如用于安全管理交易、投注或 DeFi 协议的托管账户。
Solana 根据程序定义的种子和程序 ID 确定性地派生 PDA。有关生成的程序地址的更多信息,请访问 docs.solana。
你将要做的
在本指南中,你将使用 Anchor 和 Solana Playground 创建一个 Solana 程序。该程序将允许任何用户(钱包)为任何餐厅创建或编辑评论。每个条目,程序将存储评论者、餐厅名称、评分和评论。
你将学习:
- 什么是“种子(seeds)”
- 如何在你的 Solana 程序中创建 PDA
- 如何使用交易指令根据客户端输入调用 PDA
- 如何从你的客户端查找和使用 PDA
你需要的
- 使用 Anchor 的 Solana 编程的基本经验(完成我们的 计数器程序指南)
- 对 Solana 基础知识 的基本知识
- 对 JavaScript/TypeScript 和 Rust 编程语言的基本知识
- 现代网页浏览器(例如 Google Chrome)
PDA 概述
程序派生地址 (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 所需的。
-
- signer 账户必须标记为可变,因为这个账户将被写入(SOL 借记用于资助新账户)。
- system_program 将通过跨程序调用来创建新账户(始终需要使用 init 或 _init_ifneeded)。
太棒了!现在我们的结构体已定义,我们应该能够编写我们的 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:
- 点击屏幕左侧的 Build 来编译代码并检查错误。如果你严格按照我们的示例进行操作,你不应该看到任何错误。如果你看到错误,请尝试遵循错误消息以定位你的问题(通常是拼写或大小写问题)。如果你卡住了,请随时在 Discord 上给我们发消息,我们将很乐意提供帮助。
- 点击页面左侧的工具图标 ,然后点击“Deploy”。这将把程序部署到 devnet。
- 注意:Solana Playground 仍在测试阶段。我必须在部署后刷新一下,以便我的客户端类型更新到我的新函数。你可能也需要这样做。
测试你的程序
通过点击左上角的文档图标 返回到你的 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 仪表板中的日志,以快速识别和解决问题。在 我们的定价页面 上了解有关日志历史限制的更多信息。
让我们分解一下:
- 定义我们将发布的评论内容。
- 获取我们评论的 PDA。请记住,在 Solana 编程中,我们必须在每个将用于交易的账户(即使是新账户)中传递公钥。我们调用 findProgramAddress 并传入我们在程序中定义的相同种子:餐厅名称和签名者钱包。两者都被转换为字节缓冲区。
- 获取最新的区块哈希。我们需要这个来确认我们的交易是否已成功添加到区块中。
- 我们使用 Anchor 调用我们程序的 post_review(在 TypeScript 中,它的格式为 postReview)。我们必须传入在 lib.rs 中定义的 post_review 方法中的相同参数(餐厅、评论和评分)。Anchor 允许我们使用 .accounts() 传递在 ReviewAccounts 结构中定义的账户:评论 PDA、签名者和系统程序。Anchor 知道我们需要传递系统程序和签名者,因此我们不需要在这里添加。
- 最后,通过将 REVIEW_PDA 传递给 review.fetch 来获取评论。这应该返回一个类型为 Review 的对象(如我们在程序结构中定义的)。然后我们解析并记录结果。
当你准备好评论时,点击 "▶️ Run"。你应该看到该程序已记录你的评论!
因为我们设置的 PDA 依赖于餐厅和签名者密钥,你可以通过修改 RESTAURANT 在客户端中添加更多条目并再次运行。如果你以相同的 RESTAURANT 名称重新运行,程序(如实现的那样)将覆盖先前的条目。
做得很好!
总结
做得很好!理解如何创建和与 PDA 互动是开发 Solana 的最强大工具之一。想要继续练习吗?以下是几种修改此代码以继续学习的想法:
- 你如何修改这段代码,以便 create_review 和 modify_review 是两个不同的指令?
- 你如何修改这段代码,使用户可以为多个位置的餐厅提交多个条目?(提示:一个新参数 locations,并考虑如何在 PDA 衍生中使用它)
- 你如何修改这段代码,以限制用户的 Reviews 只能针对预定义的 Restaurants?(提示:尝试创建一个新的结构体 Restaurant 和一个新的函数 _addrestaurant)
如果你卡住了、有问题,或者只想谈谈你正在构建的内容,可以在 Discord 或 Twitter 上与我们联系!
我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。