Solana[part6]_社交项目1-合约部分文档

Solana[part6]_社交项目1-合约部分文档
SoniaChenSolana[part6]_社交项目1-合约部分
社交项目地址链接
一、项目概述
本项目是基于Solana区块链的去中心化社交应用合约,使用Rust语言开发。合约支持用户账户初始化、关注/取消关注其他用户、发布内容、查询粉丝列表及查询帖子等核心社交功能。通过Solana的Program Derived Address (PDA)机制管理用户数据,使用Borsh进行数据序列化/反序列化,确保链上数据的高效存储与读取。
二、核心模块与数据结构
1. 指令定义(instruction.rs)
定义了合约支持的所有操作指令,通过SocialInstruction枚举实现,包含以下指令:
| 指令名 | 功能描述 | 参数说明 |
|---|---|---|
InitializeUser |
初始化用户账户( profile 或 post 类型) | seed_type: 账户类型(“profile” 或 “post”) |
FollowUser |
关注指定用户 | user_to_follow: 被关注用户的Pubkey |
UnfollowUser |
取消关注指定用户 | user_to_unfollow: 被取消关注用户的Pubkey |
QueryFollowers |
查询当前用户的粉丝列表 | 无参数 |
PostContent |
发布内容 | content: 帖子内容字符串 |
QueryPosts |
查询用户的帖子信息 | 无参数 |
2. 状态数据结构(state.rs)
定义了链上存储的数据结构及操作方法,核心结构如下:
(1)UserProfile
存储用户的粉丝列表信息:
pub struct UserProfile {
pub data_len: u16, // 粉丝数量(与followers.len()一致,用于快速计算存储空间)
pub followers: Vec<Pubkey>, // 粉丝的Pubkey列表
}
核心方法:
new():初始化空的用户资料follow(&mut self, user: Pubkey):添加粉丝(去重)unfollow(&mut self, user_to_follow: Pubkey):移除粉丝(更新数量)
(2)UserPost
记录用户发布的帖子数量:
pub struct UserPost {
pub post_count: u16, // 帖子总数
}
核心方法:
new():初始化帖子计数器(初始为0)add_post(&mut self):发布新帖子时递增计数器get_count(&self):获取当前帖子总数
(3)Post
存储单条帖子的具体内容:
pub struct Post {
pub content: String, // 帖子内容
pub timestamp: u64, // 发布时间戳(基于Solana网络时钟)
}
核心方法:
new(content: String, timestamp: u64):创建新帖子实例
三、指令处理逻辑(processor.rs)
Processor结构体实现了所有指令的具体处理逻辑,核心流程如下:
1. initialize_user:初始化用户账户
- 功能:创建用户的PDA账户(分为
profile和post两种类型),用于存储用户资料或帖子计数器。 - 流程:
- 解析账户信息(用户账户、PDA账户、系统程序);
- 基于用户Pubkey和
seed_type(“profile"或"post”)生成PDA地址,验证传入的PDA账户是否匹配; - 计算账户所需存储空间(
profile账户根据最大粉丝数计算,post账户固定大小); - 调用系统程序创建PDA账户(预存足够租金以豁免租金);
- 初始化对应的数据结构(
UserProfile或UserPost)并序列化到PDA账户中。
2. follow_user:关注用户
- 功能:将被关注用户的Pubkey添加到当前用户的
UserProfile粉丝列表中。 - 流程:
- 解析PDA账户(存储当前用户的
UserProfile); - 读取
data_len计算当前粉丝列表的存储空间,反序列化UserProfile; - 调用
follow方法添加粉丝(自动去重); - 将更新后的
UserProfile重新序列化并写入PDA账户。
- 解析PDA账户(存储当前用户的
3. unfollow_user:取消关注用户
- 功能:从当前用户的
UserProfile粉丝列表中移除指定用户的Pubkey。 - 流程:
- 解析PDA账户(存储当前用户的
UserProfile); - 反序列化
UserProfile; - 调用
unfollow方法移除粉丝(更新data_len); - 将更新后的
UserProfile重新序列化并写入PDA账户。
- 解析PDA账户(存储当前用户的
4. query_followers:查询粉丝列表
- 功能:读取当前用户的
UserProfile并输出粉丝列表。 - 流程:
- 解析PDA账户(存储当前用户的
UserProfile); - 反序列化
UserProfile并通过msg!打印粉丝列表。
- 解析PDA账户(存储当前用户的
5. post_content:发布内容
- 功能:创建新的帖子PDA账户,存储帖子内容和时间戳,并更新用户的帖子计数器。
- 流程:
- 解析账户信息(用户账户、帖子计数器PDA、新帖子PDA、系统程序);
- 读取当前用户的
UserPost,递增post_count并重新序列化; - 基于用户Pubkey、"post"种子和最新
post_count生成新帖子的PDA地址; - 计算帖子所需存储空间(基于
Post结构体大小),创建新帖子PDA账户; - 将帖子内容和当前时间戳(通过Solana时钟获取)序列化到新帖子PDA中。
6. query_post:查询帖子信息
- 功能:读取用户的帖子计数器和指定帖子的内容。
- 流程:
- 解析账户信息(帖子计数器PDA、目标帖子PDA);
- 分别反序列化
UserPost和Post,通过msg!打印帖子数量和内容。
四、遇到的坑
1. 关于Rust借用规则(follow_user中的作用域)
{
let data = &pda_account.data.borrow(); // 不可变引用
// ... 读取data计算size
} // data的生命周期结束,不可变引用释放
原因:Rust的借用规则禁止同一作用域内同时存在不可变引用和可变引用。此处用作用域限制data(不可变引用)的生命周期,确保后续调用try_borrow_mut_data()(获取可变引用)时不会触发借用冲突。
2. 数组切片的使用(follow_user中的&data[..U16_SIZE])
let len: &[u8] = &data[..U16_SIZE]; // U16_SIZE=2
原因:UserProfile的data_len是u16类型(占2字节),存储在账户数据的前2字节。通过数组切片提取前2字节,再转换为u16,用于计算粉丝列表的总存储空间(data_len即粉丝数量)。
3. 序列化的必要性(修改数据后重新序列化)
user_profile.serialize(&mut *pda_account.try_borrow_mut_data()?)?;
原因:链上账户存储的是字节数据,user_profile是内存中的结构体实例。当结构体被修改(如添加粉丝)后,必须通过serialize将内存中的最新状态转换为字节流,写入账户数据区域,否则修改不会被持久化到链上。
4. PDA地址计算(post_content中的种子)
Pubkey::find_program_address(&[user_account.key.as_ref(), "post".as_bytes(), &[count as u8]], program_id)
原因:PDA(程序派生地址)需通过“种子+程序ID”生成,确保唯一性。此处使用:
- 用户Pubkey(区分不同用户);
- "post"字符串(标识帖子类型);
- 帖子计数器
count(区分同一用户的不同帖子);
三者组合作为种子,确保每个帖子的PDA地址唯一。
5. unwrap()与?的区别
unwrap():当Result为Ok时返回值,为Err时直接触发程序恐慌(panic),适合确定不会出错的场景(如已知序列化Post必然成功)。?:当Result为Err时将错误返回给调用者,由上层处理,适合需要传播错误的场景(如账户解析失败)。
6. create_account指令参数
system_instruction::create_account(
user_account.key, // 资金来源(创建者)
&pda, // 新账户地址(PDA)
lamports, // 初始Lamports(需覆盖租金)
space as u64, // 账户存储空间
program_id // 账户所有者(当前合约)
)
原因:创建Solana账户必须指定:资金来源(支付租金)、账户地址、存储空间(决定租金金额)、所有者(只有所有者程序可修改账户数据)。
7. invoke_signed中的signer_seeds
&[&[user_account.key.as_ref(), "post".as_bytes(), &[count as u8], &[bump_seed]]]
原因:PDA本身不是私钥控制的账户,无法直接签名。signer_seeds用于证明“当前合约有权限操作该PDA”——通过种子重新计算PDA地址,验证与目标地址一致,从而获得临时签名权限,确保账户创建的安全性。
8. mod语句调用注意
在 Rust 中,mod 语句用于告诉编译器包含某个模块的源文件。只有在模块被声明为 pub mod xxx; 后,其他文件(如 processor.rs)才能通过 use crate::xxx::… 访问其内容。
原因如下:
state.rs 只是物理存在于 src 目录下,只有在 lib.rs 里用 pub mod state; 声明后,state 模块才会被编译进 crate 并对外可见。
如果不在 lib.rs 里声明,crate::state 这个路径在其他文件里就找不到。
processor.rs 只是使用 state,但模块的“引入”必须在 crate 的根(即 lib.rs)声明。
五、程序入口(lib.rs)
定义了Solana程序的入口点process_instruction,将指令处理逻辑委托给Processor::process_instruction,符合Solana程序的标准入口规范。
六、总结
本合约通过PDA管理用户数据,结合Borsh序列化和Solana系统指令,实现了去中心化社交应用的核心功能。关键设计点包括:用PDA确保数据所有权、通过计数器和种子生成唯一帖子地址、严格遵循Rust内存安全规则处理账户数据。







