Solana[part3]_solana账户&简单的交互

Solana[part3]_solana账户&简单的交互

solana 账户

  • 数据账户,用来存储数据
    • 系统所有账户
    • 程序派生账户(PDA)
  • 程序账户,用来存储可执行程序(智能合约),其数据字段为程序的字节码,executable 标志为 true
  • 原生账户,指 Solana 上的原生程序,例如:system(系统程序,处理账户创建、转账等基础操作),stake(质押程序),以及 vote(投票程序)

账户结构体

Account

Account 结构体主要用于客户端(如 RPC 调用)获取账户的完整数据,包含账户的所有核心属性。

代码示例:

use solana_sdk::account::Account;

// 账户结构体定义(简化版)
pub struct Account {
    // 账户中的lamports数量(Sol的最小单位)
    pub lamports: u64,
    // 账户存储的二进制数据
    pub data: Vec<u8>,
    // 账户所有者的公钥(通常是程序的公钥)
    pub owner: Pubkey,
    // 标识该账户是否为可执行程序(程序账户为true,数据账户为false)
    pub executable: bool,
    // 下一次需要支付租金的 epoch
    pub rent_epoch: Epoch,
}

Account Info

AccountInfo 是智能合约程序中用于访问账户的结构体,包含更多与程序执行相关的元数据(如签名状态、可写性),通常通过参数传递给程序入口函数。

代码示例:

use solana_program::account_info::AccountInfo;

// 账户信息结构体(简化版)
pub struct AccountInfo<'a> {
    // 账户的公钥
    pub key: &'a Pubkey,
    // 标识该账户是否为交易签名者
    pub is_signer: bool,
    // 标识该账户在本次交易中是否可写
    pub is_writable: bool,
    // 账户中lamports的可变引用
    pub lamports: Rc<RefCell<&'a mut u64>>,
    // 账户数据的可变引用
    pub data: Rc<RefCell<&'a mut [u8]>>,
    // 账户所有者的公钥
    pub owner: &'a Pubkey,
    // 标识该账户是否为可执行程序
    pub executable: bool,
    // 租金相关的epoch信息
    pub rent_epoch: Epoch,
}

对比

特性 Account(客户端) AccountInfo(程序内)
使用场景 客户端查询账户数据(如 RPC 返回) 智能合约中操作账户(函数参数)
核心数据 包含账户完整静态数据 包含数据的可变引用及操作权限标记
签名 / 可写标记 无(客户端无需关心交易执行时的权限) 有(is_signer/is_writable,用于程序验证)
数据访问方式 不可变(客户端只读) 可变引用(程序可修改数据)

要点

  • 账户是用来存放数据的基本单元,所有链上数据均存储在账户中
  • 每个账户都有一个独一无二的地址(公钥),由 32 字节组成
  • 每个账户大小不能超过 10MB,且大小是静态的(创建后无法动态扩容,需提前规划)
  • 账户数据存储需要付租金:如果账户余额低于租金阈值,会被系统回收;若存入足够 lamports(约 2 年租金),可成为rent-exempt(免租金)状态
  • 默认的账户所有者是系统程序11111111111111111111111111111111),只有所有者程序有权修改账户数据

程序派生账户(PDA)

相关文档:https://solana.com/zh/docs/core/pda

程序派生账户(PDA)是由智能合约程序通过特定算法生成的特殊账户,无对应私钥,仅由生成它的程序控制。

注意事项

  1. 不能直接签名交易
    PDA 没有私钥,无法像普通账户那样签名交易,只能由其关联的程序控制操作(确保安全性)。
  2. 地址碰撞的可能性
    理论上,相同程序 ID 和种子可能生成相同 PDA 地址(概率极低)。需通过唯一种子(如用户公钥 + 时间戳)避免碰撞。
  3. 种子长度限制
    种子组合的总长度不能超过 32 字节,超过时需哈希处理(如用sha256压缩)。
  4. 生成成本
    PDA 通过find_program_address函数生成(内部调用sha256哈希),频繁生成会增加链上计算成本。
  5. 单一程序访问
    仅生成 PDA 的程序可修改其数据,跨程序共享需特殊设计(如权限委托)。
  6. 存储限制
    作为数据账户时,大小受 10MB 限制,超大数据需拆分到多个 PDA。

应用场景

  1. 用户状态管理:存储用户在 DApp 中的资产、等级等数据
  2. 去中心化金融 (DeFi) 协议:存储流动性池、借贷记录等
  3. NFT 元数据存储:关联 NFT 的属性、创作者信息等
  4. DAO 投票系统:记录提案、投票结果等
  5. 时间锁合约:存储待执行的定时交易
  6. 多签钱包:存储签名阈值、授权列表等
  7. 去中心化身份验证:存储用户身份凭证

生成 PDA 的代码示例

use solana_program::{program_pack::Pack, pubkey::Pubkey};

// 生成PDA
fn create_pda(program_id: &Pubkey, seeds: &[&[u8]]) -> (Pubkey, u8) {
    // 生成PDA地址和bump(用于确保地址不在Ed25519曲线内)
    Pubkey::find_program_address(seeds, program_id)
}

// 示例:用用户公钥和"profile"作为种子生成PDA
let user_pubkey = Pubkey::from_str("...").unwrap();
let seeds = &[b"profile", user_pubkey.as_ref()];
let (pda_address, bump) = create_pda(&program_id, seeds);
println!("PDA地址: {}", pda_address);
println!("Bump值: {}", bump);

Rust库

  • solana_client:客户端库,用于与 Solana RPC 节点交互(查询余额、发送交易等)
  • solana_sdk:核心 SDK,定义账户、交易、签名等基础结构
  • solana_program:智能合约开发库,包含程序开发所需的账户操作、指令处理等工具

实战

  1. 启动本地环境 solana-test-validator

    ➜  ~ solana-test-validator
    Ledger location: test-ledger
    Log: test-ledger/validator.log
    ⠁ Initializing...                                                               Waiting for fees to stabilize 1...
    Identity: GmyJV396jL2hnhcooyMs9U8UeWrBSadDYGbLrd8PvAL6
    Genesis Hash: ATymZxWB3G7W8Qyjp7AVBLVN2ZLk1ssiWsTNsehNFczA
    Version: 2.2.21
    Shred Version: 31032
    Gossip Address: 127.0.0.1:1024
    TPU Address: 127.0.0.1:1027
    JSON RPC URL: http://127.0.0.1:8899
    WebSocket PubSub URL: ws://127.0.0.1:8900
    ⠒ 03:14:40 | Processed Slot: 21992 | Confirmed Slot: 21992 | Finalized Slot: 219
    
  2. 配置本地环境

    # 查看当前配置
    solana config get
    
    # 切换到本地测试网
    solana config set --url http://127.0.0.1:8899
    
    # 确认配置生效
    solana config get
    # 输出应显示RPC URL为本地地址
  3. 创建本地账户

    # 生成新账户(保存到文件)
    solana-keygen new --outfile my-wallet.json
    
    # 查看账户公钥
    solana address --keypair my-wallet.json
  4. 给账户空投 SOL

    # 向账户空投1 SOL(1 SOL = 1e9 lamports)
    solana airdrop 1 <你的账户公钥> --url http://127.0.0.1:8899
    
    # 检查余额
    solana balance <你的账户公钥>
  5. 使用sdk

    • 获取账户信息

      use solana_client::rpc_client::RpcClient;
         use solana_sdk::{pubkey::Pubkey, signature};
         use std::str::FromStr;
         fn main() {
             // 创建solana连接
             let rpc_url = "http://127.0.0.1:8899";
             let client = RpcClient::new(rpc_url);
         
             // 指定你要查询的余额账户公钥
             // 接收空投账户
             let account_pubkey = Pubkey::from_str("FcKkQZRxD5P6JwGv58vGRAcX3CkjbX8oqFiygz6ohceU").unwrap();
         
             // 获取账户余额
             match client.get_balance(&account_pubkey) {
                 Ok(balance) => {
                     println!("账户余额为: {}", balance);
                 }
                 Err(err) => {
                     eprintln!("获取账户余额时出错: {}", err);
                 }
             }
         }
    • 空投sol

      use solana_client::rpc_client::RpcClient;
         use solana_sdk::{pubkey::Pubkey, signature};
         use std::str::FromStr;
         fn main() {
             // 创建solana连接
             let rpc_url = "http://127.0.0.1:8899";
             let client = RpcClient::new(rpc_url);
         
             // 指定你要查询的余额账户公钥
             // 接收空投账户
             let account_pubkey = Pubkey::from_str("FcKkQZRxD5P6JwGv58vGRAcX3CkjbX8oqFiygz6ohceU").unwrap();
         
             // 定义空投数量
             let amount = 1 * 1_000_000_000;
         
             match client.request_airdrop(&account_pubkey, amount) {
                 Ok(signature) => {
                     println!("空投成功,签名为: {}", signature);
                 }
                 Err(err) => {
                     eprintln!("空投时出错: {}", err);
                 }
             }
         
             // 获取账户余额
             match client.get_balance(&account_pubkey) {
                 Ok(balance) => {
                     println!("账户余额为: {}", balance);
                 }
                 Err(err) => {
                     eprintln!("获取账户余额时出错: {}", err);
                 }
             }
         }
         ----------------
         空投成功,签名为: 36wYSruZNjLDiBvxG2XGNJDEDi2fmSZs9ViwYMAhEPJdpoTJMtdg5gvgRCC4VpXuDogC8YuDywCLE1ffcCDCVVnQ
         账户余额为: 500000000000000000
    • 转移sol

      use solana_client::rpc_client::RpcClient;
         use solana_sdk::signature::Signer;
         use solana_sdk::signature::read_keypair_file;
         use solana_sdk::system_instruction::transfer;
         use solana_sdk::transaction;
         use solana_sdk::{pubkey::Pubkey, signature};
         use std::str::FromStr;
         fn main() {
             // 创建solana连接s
             let rpc_url = "http://127.0.0.1:8899";
             let client = RpcClient::new(rpc_url);
         
         
             // 设置接收方
             let receive = Pubkey::from_str("6qkpaXM6Q9z9rsJQ4qpHV8soGmw2uVTFdJ8KvHR32GDe").unwrap();
         
             // 设置发送方,需要通过签名获取
             let sender = read_keypair_file("/Users/tinachan/.config/solana/id.json")
                 .expect("failed to read keypair file");
         
             // 定义空投数量 最小单位是lamports
             let amount = 1 * 1_000_000_000;
         
             // 创建转账的指令
             let transfer = transfer(&sender.pubkey(), &receive, amount);
         
             // 创建交易
             let recent_blockhash = client.get_latest_blockhash().unwrap();
             let transaction = transaction::Transaction::new_signed_with_payer(
                 &[transfer],
                 Some(&sender.pubkey()),
                 &[&sender],
                 recent_blockhash,
             );
             let result = client.send_and_confirm_transaction(&transaction);
             match result {
                 Ok(signature) => {
                     println!("交易成功,签名为: {}", signature);
                 }
                 Err(err) => {
                     eprintln!("交易时出错: {}", err);
                 }
             }
             
         }
         ----------------
         交易成功,签名为: 5yPGuuN7hbVqv1wTvsSryYG6rfaW6FNWTyih2PpwpSPxywjZ6LLMBGxdTjo59ZDdt8DfPShiq4KU4KXXvPBbevLc

    总结:(代补充)

  6. 通过JsonRpc获取账户信息 https://solana.com/zh/docs/rpc/http/getaccountinfo

    • 查看官网文档,挑选getaccountinfo来玩玩

      curl https://api.devnet.solana.com -s -X \
        POST -H "Content-Type: application/json" -d ' 
        {
          "jsonrpc": "2.0",
          "id": 1,
          "method": "getAccountInfo",
          "params": [
            "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg",
            {
              "commitment": "finalized",
              "encoding": "base58"
            }
          ]
        }
      '
    • 替换自己的账户pubkey以及本地运行链接

      ➜  sol git:(master)curl http://127.0.0.1:8899 -s -X \
        POST -H "Content-Type: application/json" -d '
        {
          "jsonrpc": "2.0",
          "id": 1,
          "method": "getAccountInfo",
          "params": [
            "6qkpaXM6Q9z9rsJQ4qpHV8soGmw2uVTFdJ8KvHR32GDe",
            {
              "commitment": "finalized",
              "encoding": "base58"
            }
          ]
        }
      '
      {"jsonrpc":"2.0","result":{"context":{"apiVersion":"2.2.21","slot":33045},"value":{"data":["","base58"],"executable":false,"lamports":1000000000,"owner":"11111111111111111111111111111111","rentEpoch":18446744073709551615,"space":0}},"id":1}