{"content":{"title":"如何在 Solana Anchor 程序中使用程序派生地址","body":">- 原文链接：[www.quicknode.com/guides...](https://www.quicknode.com/guides/solana-development/anchor/how-to-use-program-derived-addresses)\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)，校对：[翻译小组](https://learnblockchain.cn/people/412)\r\n>- 本文链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/10740)\r\n    \r\n## 概述\r\n\r\nSolana 编程的重要组成部分是程序派生地址 (PDA)。 PDA 是特定的地址，程序可以以编程方式生成有效的交易签名。这使得程序能够提供无信任服务，例如用于安全管理交易、投注或 DeFi 协议的托管账户。\r\n\r\nSolana 根据程序定义的种子和程序 ID 确定性地派生 PDA。有关生成的程序地址的更多信息，请访问 [docs.solana](https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses)。\r\n\r\n### 你将要做的\r\n\r\n在本指南中，你将使用 Anchor 和 Solana Playground 创建一个 Solana 程序。该程序将允许任何用户（钱包）为任何餐厅创建或编辑评论。每个条目，程序将存储评论者、餐厅名称、评分和评论。\r\n\r\n你将学习：\r\n\r\n*   什么是“种子(seeds)”\r\n*   如何在你的 Solana 程序中创建 PDA\r\n*   如何使用交易指令根据客户端输入调用 PDA\r\n*   如何从你的客户端查找和使用 PDA\r\n\r\n### 你需要的\r\n\r\n*   使用 Anchor 的 Solana 编程的基本经验（完成我们的 [计数器程序指南](https://learnblockchain.cn/article/10707)）\r\n*   对 [Solana 基础知识](https://learnblockchain.cn/article/10553) 的基本知识\r\n*   对 JavaScript/TypeScript 和 Rust 编程语言的基本知识\r\n*   现代网页浏览器（例如 [Google Chrome](https://www.google.com/chrome/)）\r\n\r\n## PDA 概述\r\n\r\n程序派生地址 (PDA) 是一种在 Solana 区块链上的账户类型，它与程序关联并由程序拥有，而不是特定用户或账户。 PDA 允许我们创建唯一数据关联、管理托管余额以及处理许多其他无信任应用程序。 PDA 是通过传递一组种子（例如，escrow_account、signer_id 等）和程序 ID 确定性生成的。与典型的密钥对不同，PDA 没有对应的私钥。它们是通过将种子和程序 ID 通过 sha256 哈希函数传递，寻找不在 [ed25519 椭圆曲线](https://en.wikipedia.org/wiki/Curve25519) 上的地址（在曲线上的地址是密钥对）。\"bump\" 实际上是我们用来确定性地查找 PDA 的一组距离曲线的距离。\r\n\r\n在我们的程序中，我们将使用 PDA 来存储每个评论。我们将允许任何用户为任何餐厅生成评论。为此，我们需要确定性生成每个独特评论者-餐厅对提交的 PDA。下面的图示说明了这在实践中如何工作：\r\n\r\n![PDA](https://img.learnblockchain.cn/attachments/migrate/1739439956189)\r\n\r\n正如你所看到的，任何评论者可能与他们评论的每个餐厅关联有任何数量的 PDA。如果这尚不完全清楚，不必担心。练习会让这些概念更容易理解。让我们开始构建吧！\r\n\r\n## 启动你的项目\r\n\r\n在 [Solana Playground](https://beta.solpg.io/) 上创建一个新的 Anchor 项目。打开 **lib.rs**。删除现有内容，并粘贴以下代码：\r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\ndeclare_id!(\"11111111111111111111111111111111\");\r\n\r\n#[program]\r\nmod restaurant_review {\r\n    use super::*;\r\n    pub fn post_review(ctx: Context<ReviewAccounts>, restaurant: String, review: String, rating: u8) -> Result<()> {\r\n        msg!(\"新餐厅评论！\");\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[derive(Accounts)]\r\n#[instruction(restaurant: String, review: String)]\r\npub struct ReviewAccounts<'info> {}\r\n\r\n#[account]\r\npub struct Review {\r\n\r\n}\r\n```\r\n\r\n### 创建并连接钱包\r\n\r\n如果你使用过 Solana Playground 并且已经有一个余额连接的钱包，可以跳过这一步。\r\n\r\n由于该项目仅用于演示目的，我们可以使用一个“临时”钱包。Solana Playground 使得创建一个钱包变得简单。你应该在浏览器窗口的左下角看到一个红点“未连接”。点击它：\r\n\r\n![未连接](https://img.learnblockchain.cn/attachments/migrate/1739439956543)\r\n\r\nSolana Playground 将为你生成一个钱包（或你可以导入自己的）。如果愿意，可以将其保存以备后用，并在准备好后点击继续。一个新钱包将被初始化并连接到 Solana devnet。Solana Playground 会自动空投一些 SOL 到你的新钱包，但我们将请求额外的一点，以确保我们有足够的资金来部署我们的程序。在浏览器终端中，你可以使用 Solana CLI 命令。输入 **solana airdrop 2** 将 2 SOL 空投到你的钱包。你的钱包现在应已连接到 devnet，余额为 6 SOL：\r\n\r\n![空投](https://img.learnblockchain.cn/attachments/migrate/1739439956572)\r\n\r\n你准备好了！让我们开始构建吧！\r\n\r\n## 定义评论账户\r\n\r\n首先定义我们的结构是有帮助的。最终，我们将在一个称为 _Review_ 的数据账户中存储每个评论。每个 _Review_ 账户将存储四个元素：\r\n\r\n*   评论者的公钥，\r\n*   他们要评论的餐厅，\r\n*   数字评分，以及\r\n*   简短的评论\r\n\r\n在 **lib.rs** 中，找到你的 _Review_ 的账户定义，并将其替换为：\r\n\r\n```rust\r\n#[account]\r\npub struct Review {\r\n    pub reviewer: Pubkey,\r\n    pub restaurant: String,\r\n    pub review: String,\r\n    pub rating: u8\r\n}\r\n```\r\n\r\n每当提交或更新评论时，它都应采用这种形式。现在，让我们创建我们的账户上下文，以确保我们将我们的评论保存在正确的 PDA 中。\r\n\r\n## 创建评论账户上下文\r\n\r\n正如你从以前的练习中所知道的，我们的上下文对于定义我们必须传递给程序的账户至关重要。我们的程序将需要能够创建（并支付）一个新的 _Review_ 账户，如果它尚不存在。因此，我们将需要传递系统程序、我们的签名钱包和 _Review_ 的 PDA。\r\n\r\n找到你的 _ReviewAccounts_ 结构占位符，添加三个必需账户：**review**、**signer** 和 **system_program**：\r\n\r\n```rust\r\n#[derive(Accounts)]\r\n#[instruction(restaurant: String)]\r\npub struct ReviewAccounts<'info> {\r\n    #[account(\r\n        init_if_needed,\r\n        payer = signer,\r\n        space = 500,\r\n        seeds = [restaurant.as_bytes().as_ref(), signer.key().as_ref()],\r\n        bump\r\n    )]\r\n    pub review: Account<'info, Review>,\r\n    #[account(mut)]\r\n    pub signer: Signer<'info>,\r\n    pub system_program: Program<'info, System>,\r\n}\r\n```\r\n\r\n让我们分解一下。\r\n\r\n*   首先，我们必须传递我们的 **review** 账户。这将最终是存储我们评论数据的地方。我们必须在 **review** 账户属性中传递几个约束：\r\n    \r\n    *   _init_if_needed_ 用于在账户不存在时创建账户。\r\n    \r\n    *   _payer_ 签署交易的钱包将为新账户支付租金（需要使用 _init_ 或 _init_if_needed_）。\r\n    *   _space_ 新账户应该持有的字节数（需要使用 _init_ 或 _init_if_needed_）。 _注意：我们在这里随意使用 200 字节。这可能限制用户输入评论的长度。我们将在未来的指南中讨论动态空间。_\r\n    *   _seeds_ 和 _bump_ 将检查传递的账户是否是由当前正在执行的程序、种子和 bump 派生的 PDA。因为我们希望任何用户能够评论任何独特餐厅（仅一次），我们将传递我们在评论的餐厅名称和签名者的密钥。我们需要将这两个对象引用为 _Byte Buffer_，这正是 Solana 的 **find_program_address** 查找 PDA 所需的。\r\n*   **signer** 账户必须标记为可变，因为这个账户将被写入（SOL 借记用于资助新账户）。\r\n*   **system_program** 将通过跨程序调用来创建新账户（始终需要使用 _init_ 或 _init_if_needed_）。\r\n\r\n太棒了！现在我们的结构体已定义，我们应该能够编写我们的 **post_review** 函数。\r\n\r\n## 创建一个发布评论函数 \r\n\r\n尽管 Anchor 在后台会执行很多魔法，我们仍然需要一个函数来设置我们新账户中的数据。为此，我们必须传入我们的客户端输入（之前步骤中定义的账户上下文、餐厅、评论和评分）。\r\n\r\n找到你的 **post_review** 占位符，并用以下代码替换它：\r\n\r\n```rust\r\n#[program]\r\nmod restaurant_review {\r\n    use super::*;\r\n\r\n    pub fn post_review(ctx: Context<ReviewAccounts>, restaurant: String, review: String, rating: u8) -> Result<()> {\r\n        let new_review = &mut ctx.accounts.review;\r\n        new_review.reviewer = ctx.accounts.signer.key();\r\n        new_review.restaurant = restaurant;\r\n        new_review.review = review;\r\n        new_review.rating = rating;\r\n        msg!(\"Restaurant review for {} - {} stars\", new_review.restaurant, new_review.rating);\r\n        msg!(\"Review: {}\", new_review.review);\r\n        Ok(())\r\n    }\r\n}\r\n```\r\n\r\n首先，我们需要让我们的程序知道传入的 PDA 账户是可变的（`&_` 允许我们通过引用访问账户，而 `mut` 使账户可变），我们将其定义为 **new_review**。然后对于 _Review_ 结构体的每个元素，我们将 **new_review** 的值设置为客户端传入的参数（并将 **reviewer** 设置为 **signer**）。\r\n\r\n最后，我们将评论记录到 Solana 消息日志中。\r\n\r\n在部署生产应用程序时，你将希望在指令中包含一些错误处理和保护，但这对于我们今天的目的来说应该可以。我们将在未来的指南中讨论错误处理。\r\n\r\n## 部署你的程序 \r\n\r\n在测试我们的程序之前，我们需要编译并将其部署到 Devnet：\r\n\r\n* 点击屏幕左侧的 **🔧 Build** 来编译代码并检查错误。如果你严格按照我们的示例进行操作，你不应该看到任何错误。如果你看到错误，请尝试遵循错误消息以定位你的问题（通常是拼写或大小写问题）。如果你卡住了，请随时在 [Discord](https://discord.gg/quicknode) 上给我们发消息，我们将很乐意提供帮助。\r\n* 点击页面左侧的工具图标 🛠，然后点击“Deploy”。这将把程序部署到 devnet。\r\n* _注意：Solana Playground 仍在测试阶段。我必须在部署后刷新一下，以便我的客户端类型更新到我的新函数。你可能也需要这样做。_\r\n\r\n## 测试你的程序 \r\n\r\n通过点击左上角的文档图标 📑 返回到你的 Solana Playground 浏览器，并导航到 **client.ts**。我们将编写一个简单的脚本来调用我们的程序。\r\n\r\n```javascript\r\n// Step 1 - Define Review Inputs\r\nconst RESTAURANT = \"Quick Eats\";\r\nconst RATING = 5;\r\nconst REVIEW = \"Always super fast!\";\r\n\r\n// Step 2 - Fetch the PDA of our Review account\r\nconst [REVIEW_PDA] = await anchor.web3.PublicKey.findProgramAddress(\r\n  [Buffer.from(RESTAURANT), pg.wallet.publicKey.toBuffer()],\r\n  pg.program.programId\r\n);\r\nconsole.log(`Reviewer: ${pg.wallet.publicKey.toString()}`);\r\nconsole.log(`Review PDA: ${REVIEW_PDA.toString()}`);\r\n\r\n// Step 3 - Fetch Latest Blockhash\r\nlet latestBlockhash = await pg.connection.getLatestBlockhash('finalized');\r\n\r\n// Step 4 - Send and Confirm the Transaction\r\nconst tx = await pg.program.methods  \r\n  .postReview(\r\n    RESTAURANT,\r\n    REVIEW,\r\n    RATING  \r\n  )\r\n  .accounts({ review: REVIEW_PDA })  \r\n  .transaction();\r\nconst txId = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair]);\r\nconsole.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`);\r\n\r\n// Step 5 - Fetch the data account and log results\r\nconst data = await pg.program.account.review.fetch(REVIEW_PDA);\r\nconsole.log(`Reviewer: `,data.reviewer.toString());\r\nconsole.log(`Restaurant: `,data.restaurant);\r\nconsole.log(`Review: `,data.review);\r\nconsole.log(`Rating: `,data.rating);\r\n```\r\n\r\n#### 简化调试的日志\r\n\r\n现在你可以访问 RPC 端点的日志，帮助你更有效地排查问题。如果你在 RPC 调用中遇到问题，只需检查 QuickNode 仪表板中的日志，以快速识别和解决问题。在 [我们的定价页面](https://www.quicknode.com/pricing#features) 上了解有关日志历史限制的更多信息。\r\n\r\n让我们分解一下：\r\n\r\n1. 定义我们将发布的评论内容。\r\n2. 获取我们评论的 PDA。请记住，在 Solana 编程中，我们必须在每个将用于交易的账户（即使是新账户）中传递公钥。我们调用 **findProgramAddress** 并传入我们在程序中定义的相同种子：餐厅名称和签名者钱包。两者都被转换为字节缓冲区。\r\n3. 获取最新的区块哈希。我们需要这个来确认我们的交易是否已成功添加到区块中。\r\n4. 我们使用 Anchor 调用我们程序的 **post_review**（在 TypeScript 中，它的格式为 **postReview**）。我们必须传入在 **lib.rs** 中定义的 **post_review** 方法中的相同参数（餐厅、评论和评分）。Anchor 允许我们使用 **.accounts()** 传递在 _ReviewAccounts_ 结构中定义的账户：评论 PDA、签名者和系统程序。Anchor 知道我们需要传递系统程序和签名者，因此我们不需要在这里添加。\r\n5. 最后，通过将 **REVIEW_PDA** 传递给 **review.fetch** 来获取评论。这应该返回一个类型为 _Review_ 的对象（如我们在程序结构中定义的）。然后我们解析并记录结果。\r\n\r\n当你准备好评论时，点击 \"▶️ Run\"。你应该看到该程序已记录你的评论！\r\n\r\n![Reviewed](https://img.learnblockchain.cn/attachments/migrate/1739439956575)\r\n\r\n因为我们设置的 PDA 依赖于餐厅和签名者密钥，你可以通过修改 **RESTAURANT** 在客户端中添加更多条目并再次运行。如果你以相同的 **RESTAURANT** 名称重新运行，程序（如实现的那样）将覆盖先前的条目。\r\n\r\n做得很好！\r\n\r\n## 总结\r\n\r\n做得很好！理解如何创建和与 PDA 互动是开发 Solana 的最强大工具之一。想要继续练习吗？以下是几种修改此代码以继续学习的想法：\r\n\r\n* 你如何修改这段代码，以便 **create_review** 和 **modify_review** 是两个不同的指令？\r\n* 你如何修改这段代码，使用户可以为多个位置的餐厅提交多个条目？（提示：一个新参数 **locations**，并考虑如何在 PDA 衍生中使用它）\r\n* 你如何修改这段代码，以限制用户的 _Reviews_ 只能针对预定义的 _Restaurants_？（提示：尝试创建一个新的结构体 _Restaurant_ 和一个新的函数 _add_restaurant_）\r\n\r\n如果你卡住了、有问题，或者只想谈谈你正在构建的内容，可以在 [Discord](https://discord.gg/quicknode) 或 [Twitter](https://twitter.com/QuickNode) 上与我们联系！\r\n\r\n> 我是 [AI 翻译官](https://learnblockchain.cn/people/19584)，为大家转译优秀英文文章，如有翻译不通的地方，在[这里](https://github.com/lbc-team/Pioneer/blob/master/translations/10740.md)修改，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/25306","address":null},"history":"bafkreib72nophufrwxiwirb5dzv53uomddsxlme6swvccbtk7ujiu4x2p4","timestamp":1739441113,"version":1}