Program Derived Address

在本节中,您将学习如何构建一个基本的创建、读取、更新、删除(CRUD)程序。

本指南演示了一个简单的程序,用户可以创建、更新和删除消息。每条消息都存在于一个由程序本身(Program Derived Address 或 PDA)派生的确定性地址的账户中。

本指南将带您逐步构建和测试一个使用 Anchor 框架的 Solana 程序,同时演示 Program Derived Addresses (PDAs)。有关更多详细信息,请参阅 Program Derived Addresses 页面。

作为参考,您可以在完成 PDA 和 Cross-Program Invocation (CPI) 部分后查看最终代码

初始代码

首先打开此Solana Playground 链接,其中包含初始代码。然后点击“导入”按钮,将程序添加到您的 Solana Playground 项目中。

导入导入

lib.rs 文件中,您会找到一个包含 createupdatedelete 指令的程序,您将在接下来的步骤中添加这些指令。

lib.rs
use anchor_lang::prelude::*;
declare_id!("8KPzbM2Cwn4Yjak7QYAEH9wyoQh86NcBicaLuzPaejdw");
#[program]
pub mod pda {
use super::*;
pub fn create(_ctx: Context<Create>) -> Result<()> {
Ok(())
}
pub fn update(_ctx: Context<Update>) -> Result<()> {
Ok(())
}
pub fn delete(_ctx: Context<Delete>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Create {}
#[derive(Accounts)]
pub struct Update {}
#[derive(Accounts)]
pub struct Delete {}
#[account]
pub struct MessageAccount {}

在开始之前,在 Playground 终端中运行 build,以检查初始程序是否成功构建。

Terminal
$
build

定义消息账户类型

首先,定义程序创建的消息账户的结构。此结构定义了程序创建的账户中要存储的数据。

lib.rs 中,使用以下内容更新 MessageAccount 结构:

lib.rs
#[account]
pub struct MessageAccount {
pub user: Pubkey,
pub message: String,
pub bump: u8,
}

再次构建程序,在终端中运行 build

Terminal
$
build

此代码定义了要存储在消息账户上的数据。接下来,您将添加程序指令。

添加创建指令

现在,添加 create 指令,用于创建和初始化 MessageAccount

首先,通过更新 Create 结构体定义指令所需的账户,如下所示:

lib.rs
#[derive(Accounts)]
#[instruction(message: String)]
pub struct Create<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
seeds = [b"message", user.key().as_ref()],
bump,
payer = user,
space = 8 + 32 + 4 + message.len() + 1
)]
pub message_account: Account<'info, MessageAccount>,
pub system_program: Program<'info, System>,
}

接下来,通过更新 create 函数,为 create 指令添加业务逻辑:

lib.rs
pub fn create(ctx: Context<Create>, message: String) -> Result<()> {
msg!("Create Message: {}", message);
let account_data = &mut ctx.accounts.message_account;
account_data.user = ctx.accounts.user.key();
account_data.message = message;
account_data.bump = ctx.bumps.message_account;
Ok(())
}

重新构建程序。

Terminal
$
build

添加更新指令

接下来,添加 update 指令以使用新消息更改 MessageAccount

与上一步类似,首先指定 update 指令所需的账户。

使用以下内容更新 Update 结构:

lib.rs
#[derive(Accounts)]
#[instruction(message: String)]
pub struct Update<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"message", user.key().as_ref()],
bump = message_account.bump,
realloc = 8 + 32 + 4 + message.len() + 1,
realloc::payer = user,
realloc::zero = true,
)]
pub message_account: Account<'info, MessageAccount>,
pub system_program: Program<'info, System>,
}

接下来,为 update 指令添加逻辑。

lib.rs
pub fn update(ctx: Context<Update>, message: String) -> Result<()> {
msg!("Update Message: {}", message);
let account_data = &mut ctx.accounts.message_account;
account_data.message = message;
Ok(())
}

重新构建程序

Terminal
$
build

添加删除指令

接下来,添加 delete 指令以关闭 MessageAccount

使用以下内容更新 Delete 结构体:

lib.rs
#[derive(Accounts)]
pub struct Delete<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"message", user.key().as_ref()],
bump = message_account.bump,
close = user,
)]
pub message_account: Account<'info, MessageAccount>,
}

接下来,为 delete 指令添加逻辑。

lib.rs
pub fn delete(_ctx: Context<Delete>) -> Result<()> {
msg!("Delete Message");
Ok(())
}

重新构建程序。

Terminal
$
build

部署程序

您现在已经完成了基本的 CRUD 程序。通过在 Playground 终端中运行 deploy 部署程序。

在此示例中,您将程序部署到 devnet,这是一个用于开发测试的 Solana 集群。

Playground 钱包默认连接到 devnet。确保您的 Playground 钱包中有 devnet SOL 以支付程序部署费用。从 Solana Faucet 获取 devnet SOL。

Terminal
$
deploy

设置测试文件

起始代码还包括一个位于anchor.test.ts中的测试文件。

anchor.test.ts
import { PublicKey } from "@solana/web3.js";
describe("pda", () => {
it("Create Message Account", async () => {});
it("Update Message Account", async () => {});
it("Delete Message Account", async () => {});
});

将以下代码添加到 describe() 中,但要放在 it() 部分之前。

anchor.test.ts
const program = pg.program;
const wallet = pg.wallet;
const [messagePda, messageBump] = PublicKey.findProgramAddressSync(
[Buffer.from("message"), wallet.publicKey.toBuffer()],
program.programId
);

通过在 Playground 终端中运行 test 来运行测试文件,以检查其是否按预期运行。接下来的步骤将添加实际测试。

Terminal
$
test

调用创建指令

使用以下内容更新第一个测试:

anchor.test.ts
it("Create Message Account", async () => {
const message = "Hello, World!";
const transactionSignature = await program.methods
.create(message)
.accounts({
messageAccount: messagePda
})
.rpc({ commitment: "confirmed" });
const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed"
);
console.log(JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`
);
});

调用更新指令

将第二个测试更新为以下内容:

anchor.test.ts
it("Update Message Account", async () => {
const message = "Hello, Solana!";
const transactionSignature = await program.methods
.update(message)
.accounts({
messageAccount: messagePda
})
.rpc({ commitment: "confirmed" });
const messageAccount = await program.account.messageAccount.fetch(
messagePda,
"confirmed"
);
console.log(JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`
);
});

调用删除指令

将第三个测试更新为以下内容:

anchor.test.ts
it("Delete Message Account", async () => {
const transactionSignature = await program.methods
.delete()
.accounts({
messageAccount: messagePda
})
.rpc({ commitment: "confirmed" });
const messageAccount = await program.account.messageAccount.fetchNullable(
messagePda,
"confirmed"
);
console.log("Expect Null:", JSON.stringify(messageAccount, null, 2));
console.log(
"Transaction Signature:",
`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`
);
});

运行测试

准备好测试后,在 Playground 终端中使用 test 运行测试文件。此命令会针对部署在 devnet 上的程序运行测试,并记录指向 SolanaFM 的链接以查看交易详情。

Terminal
$
test

检查 SolanaFM 链接以查看交易详情。

请注意,在此示例中,如果再次运行测试,create 指令会失败,因为 messageAccount 已作为账户存在。对于给定的 PDA,只能存在一个账户。

Is this page helpful?

Table of Contents

Edit Page