{"content":{"title":"Solana 开发系列 2 - 第一个 Solana 程序","body":"Solana系列文章第二篇，在上一篇: [理解 Solana ](https://learnblockchain.cn/article/10553)， 我们理解了 Solana 共识，账户、PDA、交易及费用、集群等。\r\n\r\n今天这篇文章我们来编写一个最简单的程序，我们将介绍：\r\n\r\n1. Anchor  开发框架\r\n2. Solana 在线 IDE ： https://beta.solpg.io/\r\n3. 以 `favorites` 程序，演示合约编译、部署、调用流程\r\n4.  解读Solana 程序代码。\r\n\r\n让我们开始吧。\r\n\r\n\r\n## Anchor 开发框架\r\n\r\n[Anchor](https://www.anchor-lang.com/) 是 Solana 程序开发框架（类似 EVM 合约开发的 [Foundry](https://learnblockchain.cn/docs/foundry/i18n/zh/) ）， 因为原生 Solana 的开发需要理解很多底层原理（账户模型、分配空间、序列化等等）\r\n\r\n对于刚接触 Solana 开发的开发者来说，使用 Anchor 是很好的选择，Anchor 提供了更友好的开发体验：\r\n\r\n1. **使用宏和属性（attribute）来简化代码**：Anchor 帮你自动生成例如复杂的序列化、去序列化或安全验证逻辑等代码。\r\n2. **内置测试框架**：内置脚手架和测试工具，让我们能够快速搭建本地测试环境并执行单元测试。\r\n3. **统一的合约结构**：Anchor 约定了代码目录结构、合约入口函数、账户声明方式等，具有更好的可读性和可维护性。\r\n4. **Anchor 命令行工具**提供了从初始化项目到编译、部署、测试的所有功能，非常适合快速迭代。\r\n   1. **`anchor init <project-name>`**：初始化 Anchor 项目。\r\n   2. **`anchor build`**：编译合约代码，生成 IDL（合约接口描述文件）。\r\n   3. **`anchor test`**：运行测试脚本。\r\n   4. **`anchor deploy`**：将合约部署到指定的网络（本地网络、测试网或主网）。\r\n\r\n\r\n\r\n## Solana 在线 IDE \r\n\r\n[Solana Playground](https://beta.solpg.io/)（类似于 EVM 的Remix）为想要快速体验 Anchor 的开发者提供了一个在线环境。相比在本地手动搭建开发环境，Playground 可以让我们免去安装 Rust、Solana CLI、Anchor CLI 等前期工作，在浏览器中便可进行编写、测试与模拟。\r\n\r\n当我们想要快速验证某段代码逻辑时，或者学习简单程序代码，Playground 非常便捷。不过，如果你打算进行更大规模的开发或生产部署，仍然建议在本地搭建完备的开发环境。\r\n\r\n\r\n\r\n## 编写第一个Solana 程序\r\n\r\n下面我们在 [Solana Playground](https://beta.solpg.io/) 中开始编写第一个Solana 程序，我们将使用一个名为 `favorites` 的示例， 这个代码来源于 [18 小时 Solana 教程 - 2024 训练营](https://www.bilibili.com/video/BV19QqnYEEwg?spm_id_from=333.788.videopod.sections&vd_source=256de7b845288ff259a7aa40777786c8) 的第一个项目。\r\n\r\n\r\n\r\n打开 https://beta.solpg.io/ ， 点击创建项目，输入项目名 `favorites`， 选择 Anchor， 点击 `Create` \r\n\r\n![](https://img.learnblockchain.cn/pics/20250108212307.png)\r\n\r\nsolpg 会默认帮我们生成一些代码：\r\n\r\n![image-20250108212822139](https://img.learnblockchain.cn/pics/20250108212823.png)\r\n\r\n`src/lib.rs`  是 Solana 合约代码，`client.ts` 是客户端代码，用用户端与合约交互的代码。 `anchor.test.ts` 是测试代码。今天我们仅关注合约部分。\r\n\r\n\r\n\r\n我们删除自动生成的代码，贴上`favorites`的代码，代码在[这里](https://github.com/lbc-team/developer-bootcamp-2024/blob/main/project-1-favorites/programs/favorites/src/lib.rs)。\r\n\r\n稍后我们来一行行解读代码，我们先体验一下代码的编译、部署以及合约调用的完整流程。\r\n\r\n### **编译合约**\r\n\r\n在 solpg 中只要点击，如下的“Build” ，就可以完成编译。\r\n![](https://img.learnblockchain.cn/pics/20250108214001.png)\r\n\r\n\r\n编译完成，在控制台有类似的输出：\r\n\r\n```\r\nBuilding...\r\nBuild successful. Completed in 5.81s.\r\n```\r\n\r\n还有会生成用户部署的二进制文件以及 IDL ， 这里我们暂时不管。如果有兴趣查看它，可以在上图左侧切换到第二个扳手图标，导出查看相应的文件。\r\n\r\n\r\n\r\n### **部署合约**\r\n\r\n部署合约需要链接到一个网络，以及一个有余额的账号。\r\n\r\n如果是第一次使用 solpg ，点击 Deploy 的时候，会出现如下创建 Playground Wallet 的提示：\r\n![](https://img.learnblockchain.cn/pics/20250108214429.png)\r\n\r\n点击“Continue”即可，刚开始创建的账号，我们需要从水龙头获取一个测试 Sol，默认情况下，solpg 应该链接的是 devnet 集群（如果没有链接devnet，可以通过下图剪头指向的设置修改）。我们可以在底部看到链接的集群信息：\r\n\r\n![](https://img.learnblockchain.cn/pics/20250108220626.png)\r\n\r\n我们可以从 https://faucet.solana.com/ 获取一些 devnet 测试Sol， 或者通过 Solpg 右上角 wallet 直接点 Airdrop：\r\n\r\n![](https://img.learnblockchain.cn/pics/20250108221031.png)\r\n\r\n\r\n\r\n准备工作做好之后，就可以进行部署：\r\n\r\n![](https://img.learnblockchain.cn/pics/20250110174654.png)\r\n\r\n点击 “Deploy” 后， 可以看到右侧 `declare_id!()` 中的程序 id 会自动修改，修改为链上的程序账户公钥。\r\n\r\n我们可以在浏览器[查看该程序账户信息](https://explorer.solana.com/address/GY2wQw39ZG6rpTB9XxSEQ5xRY58Z1r5fSAbfEhhA7mSy?cluster=devnet)：\r\n\r\n![](https://img.learnblockchain.cn/pics/20250108222047.png)\r\n\r\n\r\n\r\n### 调用函数\r\n\r\n在[favorites 合约程序里](https://github.com/lbc-team/developer-bootcamp-2024/blob/main/project-1-favorites/programs/favorites/src/lib.rs) ，定义了一个函数 `set_favorites`  ，称之为指令处理函数。上一篇我们知道了[交易](https://learnblockchain.cn/article/10553#%E4%BA%A4%E6%98%93) 会包含一个或多个指令。\r\n\r\n一条指令（Instruction）本质上是“要执行的代码 + 所需账户 + 参数数据”。在 `anchor build` 时会分析你的 `#[program]` 下的方法签名和参数，然后自动生成一个对应的 `.json` IDL文件，会列出方法名、参数类型、需要的账户等。solpg IDE 会帮我们把这些信息解析出来， 来看看来 solpg IDE如何交互。\r\n\r\n先就切换到 test 页面，可以看到已经解析出程序的指令与账户：\r\n\r\n![](https://img.learnblockchain.cn/pics/20250109155502.png)\r\n\r\n>  提示：在 **Anchor** 项目中，Rust 端（合约端）使用的是 **snake_case** （蛇形命名法）命名， 而客户前端 JavaScript / TypeScript 通常使用 camelCase （驼峰命名法）命令， Anchor 会为了与前端习惯一致，会做一个自动的命令转换。\r\n\r\n我们展开 `setFavorites` 指令，可以看到调用该指令需要的参数和需要提供的账户 :\r\n\r\n![](https://img.learnblockchain.cn/pics/20250109160527.png)\r\n\r\n `set_favorites` 做的事情是：保存自己喜欢的号码（`number`）、颜色（`color`）及习惯（`hobbies`），这些信息保存在自己地址关联的 [PDA 账户](https://learnblockchain.cn/article/10553#%E7%A8%8B%E5%BA%8F%E8%A1%8D%E7%94%9F%E5%9C%B0%E5%9D%80%EF%BC%88PDA%EF%BC%9AProgram%20Derived%20Addresses%EF%BC%89)里。\r\n\r\n因此号码（`number`）、颜色（`color`）及习惯（`hobbies`）是传递给函数的参数，而关联的账户有：\r\n\r\n `user` 用来支付该交易的费用， `favorites` 是保存这些信息的 PDA 账户。`systemProgram`  系统程序账户，例如在创建新账户**、**分配租金**、**转账 SOL，都需要系统程序账户。\r\n\r\n参数填写比较简单，`user` 账户在下拉框选择`current wallet` 即可。`favorites` 选择通过 `From seed`  创建：\r\n\r\n![](https://img.learnblockchain.cn/pics/20250109162359.png)\r\n\r\nPDA 账户创建的种子是在 程序中定义的：\r\n\r\n```\r\nseeds=[b\"favorites\", user.key().as_ref()],\r\n```\r\n\r\n种子的第一部分是 `favorites` ， 第二部分是用户的公钥， 这样每个用户都有一个对应的 PDA 账户了。\r\n\r\n![](https://img.learnblockchain.cn/pics/20250109163838.png)\r\n\r\n第一个种子写字符串\"favorites\"，第二个种子（需要先点 `Add Seed` ）， 写当前的用户公钥，点击`Generate`  创建出 PDA 账户。然后就可以点击\"Test\" 调用了。\r\n\r\n![](https://img.learnblockchain.cn/pics/20250109164526.png)\r\n\r\n我的调用记录可以在[这里](https://explorer.solana.com/tx/5ecp17rvbPWUBg9br9JnkeUZEXiWp5ivHs488aUeFbEuhyghPSSYEUhY7hEyjLqGnm7zMbp655FBp45WDaocZicc?cluster=devnet) 可以查看到，随后我们就可以通过 Fetch All 查询到所有的数据：\r\n\r\n![](https://img.learnblockchain.cn/pics/20250109165005.png)\r\n\r\n\r\n\r\n现在我们已经知道了如何惊醒合约程序的编译、部署和调用，我们继续看一下使用 Anchor 框架如何编写 Solana 合约。\r\n\r\n\r\n## 程序代码解析\r\n\r\n Solana 合约是使用 Rust 语言编写的， 学习起来门槛有一点高，不过你需要完全学会了 Rust 才动手写  Solana 合约， \r\n\r\n我们可以把Anchor  Rust 当成一门独立的语言学习，把一些用法当成是一个固定搭配，先不要深究背后的实现及原理。\r\n\r\n> 当然，我自己是一个 Rust 新手，仅供参考。\r\n\r\n\r\n### favorites 代码\r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\n// 声明程序账户地址，在部署后，被 anchor 自动更新\r\ndeclare_id!(\"ww9C83noARSQVBnqmCUmaVdbJjmiwcV9j2LkXYMoUCV\");\r\n\r\npub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;\r\n\r\n// 定义Solana 程序\r\n#[program]\r\npub mod favorites {\r\n    use super::*;\r\n\r\n    // 函数， 处理指令\r\n    pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String, hobbies: Vec<String>) -> Result<()> {\r\n        let user_public_key = context.accounts.user.key();\r\n        msg!(\"Greetings from {}\", context.program_id);\r\n        msg!(\"User {user_public_key}'s favorite number is {number}, favorite color is: {color}\");\r\n        msg!(\"User's hobbies are: {:?}\",hobbies); \r\n\r\n        context.accounts.favorites.set_inner(Favorites {\r\n            number,\r\n            color,\r\n            hobbies\r\n        });\r\n        Ok(())\r\n    }\r\n}\r\n\r\n// 定义放在 Favorites PDA 里的数据\r\n#[account]\r\n#[derive(InitSpace)]\r\npub struct Favorites {\r\n    pub number: u64,\r\n\r\n    #[max_len(50)]\r\n    pub color: String,\r\n\r\n    #[max_len(5, 50)]\r\n    pub hobbies: Vec<String>\r\n}\r\n// 定义调用set_favorites指令时，需要提供要修改的帐户。 \r\n#[derive(Accounts)]\r\npub struct SetFavorites<'info> {\r\n    #[account(mut)]\r\n    pub user: Signer<'info>,\r\n\r\n    #[account(\r\n        init_if_needed, \r\n        payer = user, \r\n        space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE, \r\n        seeds=[b\"favorites\", user.key().as_ref()],\r\n        bump\r\n    )]\r\n    pub favorites: Account<'info, Favorites>,\r\n\r\n    pub system_program: Program<'info, System>,\r\n}\r\n```\r\n\r\n ### **逐行**解析\r\n\r\n> 备注，我使用 AI 的加持，帮我解读代码\r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\n```\r\n\r\n在 Rust 中，`use xxx::*` 会把对应模块内可见的所有内容都导入到当前命名空间， 这里导入  Anchor 框架的 prelude，它包含了使用 Anchor 时大部分常用的结构和宏，例如 `Context` ，`Account`，`Signer`  等，可以查看 [Anchor 文档](https://docs.rs/anchor-lang/0.30.1/anchor_lang/index.html)\r\n\r\n\r\n\r\n```rust\r\ndeclare_id!(\"ww9C83noARSQVBnqmCUmaVdbJjmiwcV9j2LkXYMoUCV\");\r\n```\r\n\r\n`declare_id!`  宏声明此程序（Program）的唯一地址（公钥），在部署后，被 anchor 自动更新这个地址。\r\n\r\n\r\n\r\n```rust\r\npub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;\r\n```\r\n\r\n在 Anchor 中，每个账户（Account）都有一个**8 字节**的“discriminator”（鉴别符），用来标识该账户的数据结构类型。 定义常量，用来在后续计算账号所需空间（`space`）时加上这 8 字节大小。\r\n\r\n```rust\r\n#[program]\r\npub mod favorites {\r\n    use super::*;\r\n```\r\n\r\n这里开始定义一个 Anchor 的程序模块 `favorites`，   `#[program]` 是 Anchor 提供的宏，用来表明这是一个 Solana 上的可执行程序。 `use super::*;`  则是把父模块（本文件最上层）的内容导入到该模块下，从而可以使用 `Favorites`、`SetFavorites` 等定义。\r\n\r\n```rust\r\n    pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String, hobbies: Vec<String>) -> Result<()> {\r\n        let user_public_key = context.accounts.user.key();\r\n        msg!(\"Greetings from {}\", context.program_id);\r\n        msg!(\"User {user_public_key}'s favorite number is {number}, favorite color is: {color}\");\r\n\r\n        msg!(\"User's hobbies are: {:?}\", hobbies ); \r\n\r\n        context.accounts.favorites.set_inner(Favorites {\r\n            number,\r\n            color,\r\n            hobbies\r\n        });\r\n        Ok(())\r\n    }\r\n```\r\n\r\n1. **函数签名：**\r\n   - `set_favorites` 是一个上链的指令（instruction）函数。\r\n   - `context: Context<SetFavorites>` 表示这个指令需要携带哪些账户信息，在后面会从 `SetFavorites` 里看到具体定义。\r\n   - 还接受若干参数：`number`, `color`, `hobbies` \r\n   - 返回类型 `Result<()>` 表示要么成功（`Ok(())`），要么返回一个错误（`Err`），这是在 Anchor / Rust 编程中常见的模式。\r\n\r\n  2. **函数体：**\r\n     - `let user_public_key = context.accounts.user.key();`\r\n       从传入的 `Context<SetFavorites>` 的 `accounts.user` 中，`.key()`获取当前调用这条指令的用户的公钥（`Pubkey`）。\r\n     - `msg!(\"Greetings from {}\", context.program_id);`\r\n       调用 Anchor 提供的 `msg!` 宏，用于在程序日志中打印一段信息（类似 EVM 中 emit 一个事件）。\r\n     - `context.accounts.favorites.set_inner(Favorites { number, color, hobbies });`  将 `Favorites` 账户中的数据更新为用户传进来的新值。这会将该账户持久化到链上，字段包括：`number`, `color`, `hobbies`。\r\n     - `Ok(())`    指令成功执行完毕，返回一个空的成功结果。\r\n\r\n```rust\r\n#[account]\r\n#[derive(InitSpace)]\r\npub struct Favorites {\r\n    pub number: u64,\r\n\r\n    #[max_len(50)]\r\n    pub color: String,\r\n\r\n    #[max_len(5, 50)]\r\n    pub hobbies: Vec<String>\r\n}\r\n```\r\n\r\n1. `#[account]`：表示此结构体是一个可存储到 Solana 上账户数据（Account Data）中的数据类型，Anchor 会为其自动管理序列化/反序列化。\r\n\r\n2. `#[derive(InitSpace)]`：这是 Anchor 的一个属性宏，用来自动计算并提供在初始化时所需的账户空间大小（稍后会看到 `Favorites::INIT_SPACE` 用到）。\r\n\r\n3. `pub struct Favorites { ... }`  ：该结构体用于存储相关信息：\r\n   - `pub number: u64`：用户喜欢的数字。\r\n   - `#[max_len(50)] pub color: String`：用户喜欢的颜色，限制最大长度 50。\r\n   - `#[max_len(5, 50)] pub hobbies: Vec<String>`：用户的兴趣爱好列表，这里限制了向量中**最多 5 项**，且每个字符串长度最多 50。\r\n\r\n\r\n```rust\r\n#[derive(Accounts)]\r\npub struct SetFavorites<'info> {\r\n    #[account(mut)]\r\n    pub user: Signer<'info>,\r\n\r\n    #[account(\r\n        init_if_needed, \r\n        payer = user, \r\n        space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE, \r\n        seeds=[b\"favorites\", user.key().as_ref()],\r\n        bump\r\n    )]\r\n    pub favorites: Account<'info, Favorites>,\r\n\r\n    pub system_program: Program<'info, System>,\r\n}\r\n```\r\n\r\n1. `#[derive(Accounts)]`：将该结构体标注为 Anchor 的账户验证（Accounts）结构体。执行指令时，Runtime 会根据这里的定义校验和初始化所需账户。\r\n\r\n2. `pub struct SetFavorites<'info> { ... }`  ：存放 `set_favorites` 指令里需要的账户信息（包含 3 个账号）：\r\n\r\n   * `#[account(mut)] pub user: Signer<'info>` ： 表示 `user` 是一个可写账户（`mut`），而且是 `Signer`，即调用该指令时需要使用这个账户签名。\r\n\r\n```rust\r\n     #[account( init_if_needed, payer = user, space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE, seeds=[b\"favorites\", user.key().as_ref()], bump )] pub favorites: Account<'info, Favorites>,\r\n```\r\n\r\n - `init_if_needed`：如果这个账户尚未初始化，就会自动帮我们初始化。\r\n - `payer = user`：初始化账户时，需要使用 `user` 来支付租金（Solana 上存储数据的开销）。\r\n - `space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE`：给新账户分配的空间大小，包含 8 字节的 discriminator 和 `Favorites` 结构体本身的空间。\r\n - `seeds=[b\"favorites\", user.key().as_ref()]`：确定存储数据的 PDA 的种子，根据 b\"favorites\" + user 公钥来推导出来的PDA地址，  同时 anchor 会约束只有 user 能操作这个 PDA。\r\n - `bump`：用来处理 PDA 生成时的碰撞因子，由 Anchor 在运行时自动生成并校验。\r\n - ` pub favorites: Account<'info, Favorites>` 这个 `favorites` 就是我们要读写的账户数据，里面存储用户喜爱信息。\r\n- `pub system_program: Program<'info, System>,` ： 表示系统程序账户。因为我们要初始化一个新账户，需要使用系统程序来完成创建、分配空间、支付租金等操作。\r\n\r\n\r\n\r\n## 总结\r\n\r\n我们借助简单的 `favorites` 项目，演示了如何 Solpg  来进行 solana 程序的编译、部署和交互。 也详细解读了 `favorites`  合约程序如何编写的，几个关键点：\r\n\r\n1. 导入 `use anchor_lang::prelude::*;` 以便使用`anchor`定义的宏和模块。\r\n2. 通过 `declare_id!` 指定了该程序在 Solana 上的公钥地址。\r\n\r\n3. 使用 `#[program]` 宏将rust 模块转换为 Solana 程序，模块类的函数，都是指令处理函数\r\n4. 通过 `#[account]` 定义PDA 数据存储\r\n5.  通过 `#[derive(Accounts)] ` 描述调用该指令需要提供的账户。"},"author":{"user":"https://learnblockchain.cn/people/15","address":"0xD6823f807C45eFDC56c9aE8Db0226CA10af6E8AB"},"history":"bafkreigsrhki5wah4dthdhheig6anub3vn3gzug5gwy7fyrkf62ritv2cy","timestamp":1736937353,"version":1}