跳至主要內容

如何在您的 Solana Anchor 程式中使用程式衍生的位址

更新於
2025年11月12日

閱讀時間 10 分鐘

概覽

Solana 程式設計中的一個關鍵元件是「程式衍生地址」(PDA)。PDA 是一組特殊地址,程式可透過程式化方式為其生成有效的交易簽名。這使程式能夠提供無需信任的服務,例如用於安全管理交易、投注或 DeFi 協議的託管帳戶。

Solana 會根據程式所定義的種子及程式 ID,以確定性方式推導出 PDA。您可以在docs.solana 上找到有關生成的程式地址的更多資訊。

您將負責的工作內容

在本指南中,您將使用 Anchor 和 Solana Playground 建立一個 Solana 程式。該程式將允許任何使用者(錢包)針對任何餐廳撰寫或編輯評論。對於每則評論,程式會儲存評論者、餐廳名稱、評分以及評論內容。

您將學到:

  • 何謂「種子」
  • 如何在您的 Solana 程式中建立 PDA
  • 如何使用交易指令根據客戶端輸入來呼叫 PDA
  • 如何從您的客戶端搜尋並使用 PDA

您需要準備的物品

  • 具備使用 Anchor 進行 Solana 程式設計的基本經驗(請完成我們的計數器程式指南
  • Solana基礎知識
  • 具備 JavaScript/TypeScript 及 Rust 程式語言的基本知識
  • 一款現代化的網路瀏覽器(例如Google Chrome

PDA 概覽

程式衍生地址(PDA)是 Solana 區塊鏈上的一種帳戶類型,其與某個程式相關聯且由該程式擁有,而非由特定使用者或帳戶擁有。PDA 使我們能夠建立獨特的資料關聯、管理託管餘額,並處理許多其他無需信任的應用程式。PDA 是透過傳入一個種子陣列(例如:escrow_account、signer_id 等)以及程式 ID,以確定性方式生成的。 與典型的金鑰對不同,PDA 沒有對應的私鑰。其生成方式是將種子與程式 ID 輸入 SHA-256 雜湊函數,以尋找不在ed25519 橢圓曲線上的位址(位於該曲線上的位址即為金鑰對)。這種「偏移」(bump)實質上是我們用來確定性找出 PDA 的、距離曲線一定距離的位置。

在我們的程式中,我們將使用一個 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!("New restaurant review!");
Ok(())
}
}

#[derive(Accounts)]
#[instruction(restaurant: String, review: String)]
pub struct ReviewAccounts<'info> {

}

#[account]
pub struct Review {

}

建立並連結錢包

如果您曾使用過 Solana Playground,且已有連結並存有餘額的錢包,請直接跳過此步驟。

由於此專案僅供示範之用,我們可以使用一個「一次性」錢包。透過 Solana Playground 即可輕鬆建立一個。您應該會在瀏覽器視窗的左下角看到一個標有「未連線」的紅色圓點。請點擊它:

未連線

Solana Playground 將為您生成一個錢包(您也可以匯入自己的錢包)。若您願意,歡迎將其儲存以備日後使用;準備就緒後,請點擊「繼續」。 系統將建立一個新錢包並連線至 Solana 開發網。Solana Playground 會自動向您的新錢包空投一些 SOL,但我們會額外請求少量 SOL,以確保有足夠的餘額來部署我們的程式。在瀏覽器終端機中,您可以使用 Solana CLI 指令。輸入`solana airdrop 2` 即可將 2 SOL 空投至您的錢包。此時您的錢包應已連線至開發網,餘額為 6 SOL:

空投

你已經準備好了!讓我們開始動手吧!

定義審核帳戶

先定義我們的結構體會很有幫助。最終,我們將把每則評論儲存於一個名為Review 的資料帳戶中。每個Review帳戶將儲存四個元素:

  • 審閱者的公鑰,
  • 他們即將評鑑的那家餐廳,
  • 一個數值評分,以及
  • 簡短回顧

lib.rs 檔案中,找到您用於Review的帳戶定義,並將其替換為:

#[account]
pub struct Review {
pub reviewer: Pubkey,
pub restaurant: String,
pub review: String,
pub rating: u8
}

每次提交或更新評論時,都應採用此格式。現在,讓我們建立「帳戶」上下文,以確保將評論儲存至正確的 PDA 中。

建立評論帳戶背景

從之前的練習中,您應該已經知道,我們的「上下文」對於定義必須傳入程式的帳戶至關重要。如果尚未存在「審核」帳戶,我們的程式需要具備建立(並支付費用)新「審核」帳戶的能力。因此,我們需要將「系統程式」、簽署者錢包以及「審核」的 PDA 傳入程式中。

找到您的ReviewAccounts結構體的佔位符,並新增三個必填帳戶:reviewsigner 以及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>,
}

讓我們來分析一下。

  • 首先,我們必須傳入我們的 評論 帳戶。這最終將是我們審查資料的儲存位置。我們必須在 評論 帳戶屬性:
    • 若帳戶不存在,則由init_if_needed負責建立該帳戶。
    • 簽署該交易的錢包持有者將支付新帳戶的租金(必須使用initinit_if_needed)。
    • 新帳戶應佔用的位元組數(使用initinit_if_needed 時為必填)。注意:此處我們隨意設定為 200 位元組。這可能會限制使用者所能輸入的評論長度。我們將在未來的指南中探討動態空間的設定。
    • seedsbump會檢查傳入的帳戶是否為由目前正在執行的程式、seeds 以及 bump 所衍生的 PDA。由於我們希望任何使用者都能檢視任何一家獨特的餐廳(僅限一次),因此我們會傳入正在檢視的餐廳名稱以及簽署者的金鑰。我們需要將這兩個物件都作為Byte Buffer 來引用,這是 Solana 的find_program_address 函式用來查找 PDA 所必需的。
  • 簽署者帳戶必須標記為可變更狀態,因為該帳戶將會被寫入(從 SOL 帳戶扣款以資助新帳戶)。
  • 系統會透過跨程式呼叫來呼叫system_program,以建立新帳戶(這在initinit_if_needed 時總是必要的)。

太棒了!既然結構體已經定義好了,我們應該可以開始撰寫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設定為簽署者)。

最後,我們將審查結果記錄至 Solana 的訊息日誌中。

在部署生產環境應用程式時,您可能會希望在指令中加入一些錯誤處理與防護措施,但就今天的目的而言,這樣做應該已經足夠。我們將在未來的指南中探討錯誤處理的相關內容。

部署您的程式

在測試我們的程式之前,我們需要先將其編譯並部署到 Devnet:

  • 點擊螢幕左側的🔧「建置」按鈕,以編譯您的程式碼並檢查錯誤。如果您完全按照我們的範例操作,應該不會看到任何錯誤。若出現錯誤,請試著根據錯誤訊息找出問題所在(通常是拼寫或大小寫的問題)。如果您遇到困難,歡迎隨時在Discord 上留言給我們,我們很樂意提供協助。
  • 點擊頁面左側的「工具」圖示 🛠,然後點擊「部署」。這將把程式部署到 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);

讓我們來分析一下:

  1. 請說明我們即將發布的評論內容。
  2. 取得我們這篇評測中提到的 PDA。請記住,在 Solana 程式設計中,我們必須將所有將要使用的帳戶(即使是新帳戶)的公鑰傳入交易中。我們呼叫findProgramAddress 函式,並傳入在程式中定義的相同種子:餐廳名稱和簽署者錢包。這兩者都會被轉換為位元組緩衝區
  3. 取得最新的區塊哈希值。我們需要這個值來確認我們的交易已成功加入該區塊。
  4. 我們使用 Anchor 來呼叫程式的post_review方法(請注意,在 TypeScript 中,其格式為postReview)。 我們必須傳入與lib.rs中的post_review方法所定義相同的參數(restaurant、review 以及 rating)。Anchor 會讓我們透過.accounts() 傳遞在ReviewAccounts結構體中定義的帳戶:review PDA、簽署者以及系統程式。Anchor 知道我們需要傳遞系統程式和簽署者,因此我們無需在此處額外指定。
  5. 最後,透過將REVIEW_PDA傳入review.fetch 來擷取評論。這應該會傳回一個類型為Review的物件(如我們程式中的 struct 所定義)。接著,我們會解析並記錄結果。

當您準備好審閱內容後,請點擊「▶️ 執行」。您應該會看到程式已記錄您的審閱內容!

已審閱

由於我們將 PDA 設定為依賴「餐廳」和「簽署者」金鑰,您可以透過修改客戶端中的RESTAURANT值並重新執行程式,來新增更多條目。若您使用相同的RESTAURANT名稱重新執行程式,該程式(依目前實作方式)將會覆寫先前的條目。

做得真棒!

總結

做得真棒!了解如何建立及與 PDA 互動,是於 Solana 上進行開發的最強大工具之一。想繼續練習嗎?以下提供幾個修改此程式碼的建議,助你持續學習:

  • 你該如何修改這段程式碼,讓`create_review` 和 `modify_review` 成為兩條不同的指令?
  • 你該如何修改這段程式碼,才能讓使用者針對擁有多個分店的餐廳建立多筆記錄?(提示:新增一個名為 locations的參數,並思考如何在 PDA 的推導中運用它)
  • 你該如何修改這段程式碼,將用戶的「評論」限制在預先定義的「餐廳」範圍內?(提示:試著建立一個新的結構體Restaurant以及一個新的函式add_restaurant

如果你遇到困難、有任何疑問,或是單純想聊聊你正在開發的專案,歡迎在DiscordTwitter 上聯絡我們!

我們 ❤️ 您的回饋!

如果您對這份指南有任何意見或建議,請告訴我們。我們非常樂意聽取您的意見。

分享這份指南