Solana[part11]_Anchor实战:用户发帖&点赞

Solana[part11]_Anchor实战:用户发帖&点赞

发帖功能(createTweet)

1. 核心数据结构

发帖功能的核心数据结构为Tweet,定义在programs/anchor_social/src/state/tweet.rs中,用于存储帖子内容及点赞数:

// programs/anchor_social/src/state/tweet.rs
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct Tweet {
    #[max_len(50)]  // 限制帖子内容最大长度为50字符
    pub body: String,  // 帖子内容
    pub like_count: u64,  // 点赞数,初始为0
}

impl Tweet {
    pub const SEED_PREFIX: &'static str = "tweet";  // PDA种子前缀

    // 初始化新帖子
    pub fn new(body: String) -> Self {
        Self {
            body,
            like_count: 0,
        }
    }
}

2. 前端API实现(创建帖子)

前端通过app/api/tweet.ts中的createTweet函数发起创建帖子的交易,核心逻辑包括生成帖子PDA、调用链上程序:

// app/api/tweet.ts
import * as anchor from "@coral-xyz/anchor";
import { program } from "./wallet";

export async function createTweet(
    wallet: anchor.Wallet,
    body: string,
): Promise<[anchor.web3.PublicKey, string]> {
    // 1. 生成用户Profile的PDA(用于关联帖子所属用户)
    const [profilePda,] = anchor.web3.PublicKey.findProgramAddressSync(
        [Buffer.from("profile"), wallet.publicKey.toBuffer()],  // 种子:"profile" + 用户公钥
        program.programId,
    );

    // 2. 获取用户Profile数据(需先创建Profile)
    const profile = await program.account.profile.fetch(profilePda);

    // 3. 生成帖子的PDA(唯一标识帖子)
    const [tweetPda] = anchor.web3.PublicKey.findProgramAddressSync(
        [
            Buffer.from("tweet"),  // 种子1:固定前缀"tweet"
            profilePda.toBuffer(),  // 种子2:用户Profile的PDA(关联用户)
            Buffer.from((profile.tweet_count + 1).toString())  // 种子3:用户的第N+1条帖子(确保唯一性)
        ],
        program.programId,
    );

    // 4. 调用链上程序创建帖子,返回帖子PDA和交易签名
    return [
        tweetPda,
        await program.methods.createTweet(body)
            .accounts({
                authority: wallet.publicKey,  // 交易发起者(帖子作者)
                tweet: tweetPda,  // 帖子账户(新建)
            })
            .rpc()  // 发送交易
    ];
}

3. 后端程序逻辑(创建帖子)

链上程序逻辑定义在programs/anchor_social/src/instructions/tweet.rs中,负责初始化帖子账户、更新用户发帖数:

// programs/anchor_social/src/instructions/tweet.rs
use crate::state::profile::*;
use crate::state::tweet::*;
use anchor_lang::prelude::*;

// 处理创建帖子的核心逻辑
pub fn create_tweet(ctx: Context<CreateTweet>, body: String) -> Result<()> {
    // 1. 更新用户Profile的发帖数(+1)
    let profile = &mut ctx.accounts.profile;
    profile.tweet_count += 1;

    // 2. 初始化帖子数据
    let tweet = Tweet::new(body);
    // 将帖子数据写入账户(set_inner用于更新Anchor账户的内部数据)
    ctx.accounts.tweet.set_inner(tweet.clone());
    Ok(())
}

// 定义创建帖子所需的账户结构
#[derive(Accounts)]
pub struct CreateTweet<'info> {
    // 帖子账户(新建)
    #[account(
        init,  // 初始化新账户
        payer = authority,  // 由authority支付账户创建费用
        space = 8 + Tweet::INIT_SPACE,  // 分配存储空间(8字节为Anchor账户前缀)
        seeds = [  // PDA种子(需与前端生成逻辑一致)
            Tweet::SEED_PREFIX.as_bytes(),  // "tweet"
            profile.key().as_ref(),  // 用户Profile的PDA
            (profile.tweet_count + 1).to_string().as_ref()  // 第N+1条帖子
        ],
        bump  // 自动计算PDA的bump值
    )]
    pub tweet: Account<'info, Tweet>,

    // 用户Profile账户(需已存在,用于记录发帖数)
    #[account(mut, seeds = [Profile::SEED_PREFIX.as_bytes(), authority.key().as_ref()], bump)]
    pub profile: Account<'info, Profile>,

    // 交易发起者(帖子作者,签名者)
    #[account(mut)]
    pub authority: Signer<'info>,

    // Solana系统程序(用于创建新账户)
    pub system_program: Program<'info, System>,
}

4. 关键技术点解析

  • PDA(Program Derived Address)设计:帖子的PDA由"tweet" + 用户Profile的PDA + 帖子序号组成,确保每个用户的每条帖子有唯一标识,且可通过种子逆向推导。
  • 状态更新依赖:创建帖子时必须先存在用户Profile(用于记录 tweet_count),否则会因无法获取Profile数据而失败。
  • 存储空间分配space = 8 + Tweet::INIT_SPACE中,8字节是Anchor账户的固定前缀(用于存储账户 discriminator),Tweet::INIT_SPACE#[derive(InitSpace)]自动计算(包含bodylike_count的空间)。

点赞功能(CreateLike)

1. 核心数据结构

点赞功能涉及两个核心数据结构:Like(存储点赞关系)和Tweet(记录点赞数)。Like定义在programs/anchor_social/src/state/like.rs中:

// programs/anchor_social/src/state/like.rs
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct Like {
    pub profile_pubkey: Pubkey,  // 点赞用户的公钥
    pub tweet_pubkey: Pubkey,  // 被点赞帖子的公钥
}

impl Like {
    pub const SEED_PREFIX: &'static str = "like";  // PDA种子前缀

    // 初始化新点赞
    pub fn new(profile_pubkey: Pubkey, tweet_pubkey: Pubkey) -> Self {
        Self {
            profile_pubkey,
            tweet_pubkey,
        }
    }
}

2. 前端API实现(创建点赞)

前端通过app/api/tweet.ts中的createLike函数发起点赞交易:

// app/api/tweet.ts
export async function createLike(
    wallet: anchor.Wallet,
    tweetPdas: anchor.web3.PublicKey  // 被点赞帖子的PDA
) {
    // 调用链上程序创建点赞,返回交易签名
    return await program.methods.createLike()
        .accounts({
            tweet: tweetPdas,  // 被点赞的帖子账户
            authority: wallet.publicKey,  // 点赞用户
        })
        .signers([wallet.payer])  // 签名者(点赞用户)
        .rpc();
}

3. 后端程序逻辑(创建点赞)

链上程序逻辑定义在programs/anchor_social/src/instructions/tweet.rs中,负责创建点赞记录、更新帖子点赞数:

// programs/anchor_social/src/instructions/tweet.rs
use crate::state::like::*;
use crate::state::tweet::*;
use anchor_lang::prelude::*;

// 处理创建点赞的核心逻辑
pub fn create_like(ctx: Context<CreateLike>) -> Result<()> {
    // 1. 更新帖子的点赞数(+1)
    let tweet = &mut ctx.accounts.tweet;
    tweet.like_count += 1;

    // 2. 初始化点赞记录(关联点赞用户和帖子)
    let like = Like::new(ctx.accounts.authority.key(), tweet.key());
    // 将点赞数据写入账户
    ctx.accounts.like.set_inner(like);
    Ok(())
}

// 定义创建点赞所需的账户结构
#[derive(Accounts)]
pub struct CreateLike<'info> {
    // 点赞记录账户(新建)
    #[account(
        init,  // 初始化新账户
        payer = authority,  // 由点赞用户支付费用
        space = 8 + Like::INIT_SPACE,  // 分配存储空间
        seeds = [  // PDA种子(确保唯一:同一用户对同一帖子只能点赞一次)
            Like::SEED_PREFIX.as_bytes(),  // "like"
            profile.key().as_ref(),  // 点赞用户的Profile PDA
            tweet.key().as_ref()  // 被点赞帖子的PDA
        ],
        bump
    )]
    pub like: Account<'info, Like>,

    // 被点赞的帖子账户(需已存在,更新点赞数)
    #[account(mut)]
    pub tweet: Account<'info, Tweet>,

    // 点赞用户的Profile账户(验证用户身份)
    #[account(mut, seeds = [Profile::SEED_PREFIX.as_bytes(), authority.key().as_ref()], bump)]
    pub profile: Account<'info, Profile>,

    // 点赞用户(签名者)
    #[account(mut)]
    pub authority: Signer<'info>,

    // 系统程序(用于创建点赞账户)
    pub system_program: Program<'info, System>,
}

4. 关键技术点解析

  • 点赞唯一性保障:点赞记录的PDA由"like" + 点赞用户Profile PDA + 帖子PDA组成,确保同一用户对同一帖子只能创建一条点赞记录(重复创建会因PDA已存在而失败)。
  • 状态联动更新:点赞时同时更新两个状态:tweet.like_count(帖子的点赞数+1)和Like账户(存储点赞关系)。
  • 依赖验证:点赞操作依赖三个前提:帖子账户已存在、点赞用户的Profile账户已存在,否则会因账户不存在而失败。

使用示例

以下是app/index.ts中发帖和点赞的调用示例,展示完整流程:

// app/index.ts
import { useDefaultWallet, useVisitorWallet } from "./api/wallet";
import { createTweet, getTweet, createLike } from "./api/tweet";

(async () => {
    const defaultWallet = useDefaultWallet();  // 发帖用户钱包
    const visitorWallet = useVisitorWallet();  // 点赞用户钱包

    // 1. 创建帖子
    const [tweetPda, createTweetTx] = await createTweet(defaultWallet, "Hello, world!");
    console.log("帖子创建成功,PDA:", tweetPda.toBase58(), "交易签名:", createTweetTx);

    // 2. 获取帖子详情(验证创建结果)
    const tweet = await getTweet(defaultWallet, tweetPda);
    console.log("帖子初始信息:", tweet);  // like_count应为0

    // 3. 对帖子点赞
    const createLikeTx = await createLike(visitorWallet, tweetPda);
    console.log("点赞成功,交易签名:", createLikeTx);

    // 4. 再次获取帖子详情(验证点赞数更新)
    const updatedTweet = await getTweet(defaultWallet, tweetPda);
    console.log("点赞后帖子信息:", updatedTweet);  // like_count应为1
})();

总结

发帖和点赞功能通过Anchor框架实现了Solana区块链上的状态管理:

  • 发帖功能通过PDA关联用户与帖子,确保唯一性并记录发帖数;
  • 点赞功能通过PDA保障唯一点赞,并联动更新帖子的点赞数;
  • 核心依赖Profile账户作为用户身份标识,所有操作均需验证账户存在性。

理解上述逻辑有助于掌握Solana上的PDA设计、账户交互及状态更新模式。