{"author":{"address":"0xD6823f807C45eFDC56c9aE8Db0226CA10af6E8AB","user":"https://learnblockchain.cn/people/15"},"content":{"body":"Solana 系列文章第 3 篇，在[上一篇](https://learnblockchain.cn/article/10569)，我们使用了 Sol 在线 IDE solpg 部署了一个简单的 `favorites` 程序，对于较大的项目，我们通常需要在本地进行工程化开发，在本地使用 Anchor 构建程序，这就是今天这篇文章的内容：\r\n\r\n1. Anchor 开发环境搭建\r\n2. 使用 `create-solana-dapp` 新建工程 \r\n3. 使用 Anchor 编译与部署合约\r\n4. 使用 Bankrun 测试合约\r\n\r\n## Anchor 开发环境搭建\r\n\r\n在开始之前，我们需要准备好开发 Solana DApp 所需的环境， 整个安装过程大体上根据 Anchor 安装说明： https://www.anchor-lang.com/docs/installation \r\n\r\nAnchor 是使用 Rust 开发的，我们需要安装 Rust 来编译Anchor，后面也需要用 Rust 来编译 Solana 合约，因此我们先安装 Rust。\r\n\r\n\r\n\r\n### 1. 安装 Rust\r\n\r\n安装 Rust 推荐使用 rustup ， 这样方便之后我们切换不同的rust 版本：\r\n\r\n```bash\r\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\r\n```\r\n\r\n在弹出的安装选项中，选择默认选项即可。\r\n\r\n![image-20250114205341575](https://img.learnblockchain.cn/pics/20250114205342.png)\r\n\r\n\u003e 如果网络不好，可能等待会比较长。\r\n\r\n\r\n\r\n安装完成后，可以用 `rustup --version` 看一下当前的版本：\r\n\r\n```bash\r\nrustup --version\r\nrustup 1.27.1 (54dd3d00f 2024-04-24)\r\ninfo: This is the version for the rustup toolchain manager, not the rustc compiler.\r\ninfo: The currently active `rustc` version is `rustc 1.79.0 (129f3b996 2024-06-10)`\r\n```\r\n\r\n可以看到我这里的版本是  rustup 1.27.1， rustc 为 1.79.0 ， 此时说明已经正确安装了 rust.\r\n\r\n\r\n\r\n### 2. 安装 Solana CLI\r\n\r\nSolana 命令行工具用于与 Solana 网络交互，包括部署和管理程序， 安装参考：https://solana.com/docs/intro/installation\r\n\r\n```bash\r\nsh -c \"$(curl -sSfL https://release.anza.xyz/stable/install)\"\r\n```\r\n\r\n`stable` 可以替换为指定的版本， 脚本执行后按提示添加环境变量，之后你可以这样验证一个安装：\r\n\r\n```\r\n\u003e solana --version\r\nsolana-cli 1.18.18 (src:83047136; feat:4215500110, client:SolanaLabs)\r\n```\r\n\r\n可以看到我这里的 solana 版本是 1.18.18\r\n\r\n\r\n\r\n### 3. 安装前端工具\r\n\r\nDApp 需要在前端和 Solana 链，我们用 NVM 来安装管理不同的 Node.js 版本， 参考[这里](https://github.com/nvm-sh/nvm/blob/master/README.md#installing-and-updating)。\r\n\r\n### 4. 安装 Anchor\r\n\r\n安装 Anchor 推荐使用 avm (Anchor version manager)  来安装，使用 `cargo` 来安装（cargo 是 rust 的包管理工具）：\r\n\r\n```bash\r\ncargo install --git https://github.com/coral-xyz/anchor avm --locked --force\r\n```\r\n\r\n我在这步安装遇到了一点问题，直到我把 rust 切换到 nightly，并且设置 CARGO_NET_GIT_FETCH_WITH_CLI 才搞定：\r\n\r\n```bash\r\n\u003e rustup install nightly\r\n\u003e export CARGO_NET_GIT_FETCH_WITH_CLI=true\r\n```\r\n\r\n\r\n\u003e 由于 Solana 和 Anchor 都在不断的更新中，所依赖的 rust 相关工具链版本也在不断变化，这些版本问题确实有一点坑。\r\n\r\n\r\n\r\n安装完成后，验证版本：\r\n\r\n```bash\r\navm --version\r\n```\r\n\r\n然后用 avm 安装 Anchor :\r\n\r\n```bash\r\n\u003e avm use latest\r\n```\r\n\r\n安装完成后，验证 anchor 版本：\r\n\r\n```bash\r\n\u003e anchro --version\r\nanchor-cli 0.30.1\r\n```\r\n\r\n这样所需要的环境就安装好了。\r\n\r\n\r\n\r\n## 新建工程\r\n\r\n[create-solana-dapp](https://github.com/solana-developers/create-solana-dapp)  是一个脚手架工具，为我们设置一个包含各种配置选项的 Solana Web 项目，包括 Next.js 客户端、Tailwind UI 库和一个简单的 Anchor 程序。\r\n\r\n\u003e 工程的完整代码，可以参考[这里](https://github.com/xilibi2003/solana_favorites_demo) 。\r\n\r\n\r\n现在我们来创建一个  `favorites ` 工程：\r\n\r\n```bash\r\nnpx create-solana-dapp\r\n```\r\n\r\n命令会提示输入项目名称、前端模版框架：\r\n\r\n![image-20250114220941879](https://img.learnblockchain.cn/pics/20250114220943.png)\r\n\r\n\r\n\r\n```bash\r\ncd favorites\r\n```\r\n\r\n项目结构有两个主要的目录：\r\n\r\n- `anchor/`：存放 Anchor 工程代码代码。\r\n- `src/`：存放前端应用代码。\r\n\r\n------\r\n\r\n## 使用 Anchor 编译与部署合约\r\n\r\n在项目创建完成后，我们可以 VS Code  打开工程，我们需要编写、编译并部署智能合约。\r\n\r\n### 1. 编写合约\r\n\r\n在 `anchor/programs` 目录下，你会看到默认生成的 Anchor 合约示例。以 `favorites` 为例，打开 `anchor/programs/favorites/src/lib.rs`，用上一篇文章的 `favorites` 替换 `lib.rs` 。\r\n\r\n### 2. 编译合约\r\n\r\n在项目根目录下，运行以下命令编译合约：\r\n\r\n```bash\r\nanchor build\r\n```\r\n\r\n你可能会遇到错误：`error: init_if_needed requires that anchor-lang be imported with the init-if-needed cargo feature`\r\n\r\n这是需要我们把 `anchor/programs/favorites/cargo.toml`  修改一下依赖的 feature ：\r\n\r\n```toml\r\n...\r\n[dependencies]\r\nanchor-lang = { version = \"0.30.1\", features = [\"init-if-needed\"] }\r\n...\r\n```\r\n\r\n`Cargo.toml` 文件是管理 Rust 项目依赖、编译配置、项目包管理及元信息的文件。**决定“这个程序是如何编译、依赖什么库，以及采用哪些 Rust 特性”**。\r\n\r\n\r\n\r\n再次运行 `anchor build ` ，执行成功后，在 **`target/`** 目录下生成一系列构建的文件，最重要的几个文件有：\r\n\r\n1. 编译出的程序（文件名：`target/deploy/\u003cprogram-name\u003e.so` ），这是最终部署到 Solana 链上的可执行文件（ELF 文件）。\r\n2. 程序的 Keypair（`target/deploy/\u003cprogram-name\u003e-keypair.json`）， 记录程序的密钥和公钥。在 `anchor deploy` 时，Anchor CLI 会使用此 Keypair 将程序部署到链上，并将其 **公钥** 注册为 **Program ID**。\r\n3. IDL 文件 （ `target/idl/\u003cprogram-name\u003e.json`） ，里面包含了程序的程序 ID，以及所有方法、数据结构等描述，客户端就是通过这个文件来进行对智能合约的调用。\r\n\r\n### 3. 部署合约\r\n\r\n部署合约前，我们需要确定部署的[集群](https://learnblockchain.cn/article/10553#Solana%20%E9%9B%86%E7%BE%A4)，以及部署程序的账号，这些是在 `Anchor.toml`中配置的。\r\n\r\n`Anchor.toml` 是 **Anchor 框架** 特有的项目配置文件，用来配置程序、网络和钱包选项。**决定\"部署到哪、用哪个钱包、项目中包含哪些程序**。\r\n\r\nAnchor.toml 包含的所有配置项，可以在[这里](https://www.anchor-lang.com/docs/manifest) 找到，当下我们使用到的有：\r\n\r\n```toml\r\n[provider]\r\ncluster = \"localnet\"\r\nwallet = \"~/.config/solana/id.json\"\r\n```\r\n\r\n#### 启动网络 \r\n\r\n这里配置的使用本地网络的集群，这个集群是通过 `solana-test-validator` 启动的。\r\n\r\n 运行   `solana-test-validator`  启动本地网络集群：\r\n\r\n```bash\r\n\u003e solana-test-validator\r\nLedger location: test-ledger\r\nLog: test-ledger/validator.log\r\n⠉ Initializing...                                                                                                                                                    Waiting for fees to stabilize 1...\r\nIdentity: Fzsq7WHnBVLCcwaeXnjYVYDsTuTT4izcPCHHFjbYasmK\r\nGenesis Hash: 3fzCr16ffwszZDrTYX3z4775PC1hMpfpxfur6HE83LAh\r\nVersion: 1.18.18\r\nShred Version: 766\r\nGossip Address: 127.0.0.1:1024\r\nTPU Address: 127.0.0.1:1027\r\nJSON RPC URL: http://127.0.0.1:8899\r\nWebSocket PubSub URL: ws://127.0.0.1:8900\r\n```\r\n\r\n#### 准备部署账号\r\n\r\n现在创建一个账号用来部署合约:\r\n\r\n```bash\r\n\u003e solana-keygen new -o /Users/emmett/.config/solana/id.json\r\nWrote new keypair to /Users/emmett/.config/solana/id.json\r\n==========================================================================\r\npubkey: 6oLiQn73H8EWnbo5sSuFx1V4KNAasBgFP39puLR9Emaw\r\n==========================================================================\r\nSave this seed phrase and your BIP39 passphrase to recover your new keypair:\r\ntest test test test test test .....\r\n==========================================================================\r\n```\r\n\r\n\r\n\r\n部署合约之前，需要确保本地有足够的 SOL 余额。你可以通过以下命令获取测试 SOL：\r\n\r\n```bash\r\n\u003e solana airdrop 2 -u http://127.0.0.1:8899\r\nRequesting airdrop of 2 SOL\r\n...\r\n```\r\n\r\n`-u` 用来指定网络集群端点。\r\n\r\n使用以下命令查询余额：\r\n\r\n```\r\nsolana balance -u http://127.0.0.1:8899\r\n```\r\n\r\n也可以在浏览器上 https://explorer.solana.com/ ，链接本地集群查询账号信息：\r\n\r\n![image-20250115223151753](https://img.learnblockchain.cn/pics/20250115223153.png)\r\n\r\n\r\n\r\n####  开始部署合约\r\n\r\n使用  `anchor deploy ` 部署合约\r\n\r\n```bash\r\n\u003e anchor deploy\r\nDeploying cluster: http://127.0.0.1:8899\r\nUpgrade authority: /Users/emmett/.config/solana/id.json\r\nDeploying program \"favorites\"...\r\nProgram path: /Users/emmett/SolanaProject/favorites/anchor/target/deploy/favorites.so...\r\nProgram Id: 2RGcHFUgJ4R8jxgQjLy1dF7p5kuUFK7aUKqyAxDcBnjH\r\n```\r\n\r\n部署成功后，控制台会打印出新部署的程序 ID。\r\n\r\n\u003e 小技巧，部署后，可以使用命令 `anchor keys sync` 把 Anchor.toml 和 lib.rs 中的  `program id`  更新一下   \r\n\r\n\r\n\r\n## 合约测试\r\n\r\n[Bankrun](https://github.com/kevinheavey/solana-bankrun) 是一个专门为 Solana 程序测试设计的、使用 NodeJS 进行测试框架, 提供了很多优秀的功能，例如：直接在内存中模拟 Solana 运行时环境，测试执行速度比传统方法快 10-100 倍，支持完整的账户状态控制（如设置账户余额、数据），丰富的测试 API（如模拟各种错误情况） 等。\r\n\r\n\r\n### 1. 安装 Bankrun\r\n\r\n在 anchor 目录下，通过以下命令安装：`anchor-bankrun`：\r\n\r\n```bash\r\nnpm install anchor-bankrun\r\n```\r\n\r\n### 2. 编写测试\r\n\r\n `create-solana-dapp`   创建工程时， 在 `anchor/tests/` 目录下，已经包含了一个测试模版文件 `favorites.spec.ts`，我们用以下代码替换 `favorites.spec.ts` 文件。\r\n\r\n```typescript\r\n// 导入必要的依赖\r\nimport * as anchor from '@coral-xyz/anchor'\r\nimport { Program } from '@coral-xyz/anchor'\r\nimport { PublicKey, SystemProgram } from '@solana/web3.js'\r\n\r\nimport { BankrunProvider, startAnchor } from \"anchor-bankrun\"; \r\n// 导入程序类型定义，这是由 Anchor 自动生成的\r\nimport { Favorites } from '../target/types/favorites'\r\n\r\n// 导入程序的 IDL (Interface Description Language) 文件\r\nconst IDL = require(\"../target/idl/favorites.json\");\r\n\r\n// 定义程序 ID，应该与你部署的程序地址匹配\r\nconst PROGRAM_ID = new PublicKey(\"coUnmi3oBUtwtd9fjeAvSsJssXh5A5xyPbhpewyzRVF\");\r\n\r\ndescribe('favorites', () =\u003e {\r\n  it('设置收藏', async () =\u003e {\r\n    // 初始化 Bankrun 测试环境\r\n    const context = await startAnchor(\"\", [{name: \"favorites\", programId: PROGRAM_ID}], []);\r\n    \r\n    // 创建 Bankrun Provider，用于与程序交互\r\n    const provider = new BankrunProvider(context);\r\n\r\n    // 使用 IDL 和 Provider 创建程序实例\r\n    const favoritesProgram = new Program\u003cFavorites\u003e(\r\n      IDL,\r\n      provider,\r\n    );\r\n    \r\n    // 计算 PDA (Program Derived Address)\r\n    // seeds: [\"favorites\", 用户公钥]\r\n    const [favoritePda] = PublicKey.findProgramAddressSync(\r\n      [\r\n        Buffer.from(\"favorites\"),\r\n        provider.wallet.publicKey.toBuffer()\r\n      ],\r\n      favoritesProgram.programId\r\n    )\r\n\r\n    // 准备测试数据\r\n    const favoriteNumber = new anchor.BN(42)  // 使用 BN 处理大数\r\n    const favoriteColor = \"red\"\r\n    const hobbies = [\"book\", \"sport\", \"programming\"]\r\n\r\n    // 调用程序的 set_favorites 指令\r\n    // Anchor 会自动推导所需的账户，无需手动指定\r\n    await favoritesProgram.methods\r\n      .setFavorites(\r\n        favoriteNumber,\r\n        favoriteColor,\r\n        hobbies\r\n      )\r\n      .rpc()\r\n\r\n    // 从链上获取账户数据并验证\r\n    const favoriteData = await favoritesProgram.account.favorites.fetch(favoritePda)\r\n    \r\n    // 验证存储的数据是否与输入匹配\r\n    expect(favoriteData.number.toString()).toEqual(favoriteNumber.toString())\r\n    expect(favoriteData.color).toEqual(favoriteColor)\r\n    expect(favoriteData.hobbies).toEqual(hobbies)\r\n  })\r\n})\r\n\r\n```\r\n\r\n在 Anchor Bankrun 测试中不需要显式指定 accounts，Anchor 会根据指令（instruction）的定义自动推导所需的账户。\r\n\r\n\r\nstartAnchor 参数， 可参考相关的[文档](https://kevinheavey.github.io/solana-bankrun/api/#functions-2) 。\r\n\r\n\r\n\r\n### 3. 运行测试\r\n\r\n在项目根目录下，运行以下命令：\r\n\r\n```bash\r\nanchor test\r\n```\r\n\r\n如果运行 `anchor test` 时出现错误：\r\n```\r\nError: Your configured rpc port: 8899 is already in use\r\n```\r\n\r\n这是因为已经有一个 `solana-test-validator` 实例正在运行，而 Anchor 在运行测试时会尝试启动一个新的本地验证节点（也使用端口8899），所以会报错。\r\n\r\n我们可以使用 `--skip-local-validator` 和 `--skip-deploy` 参数来跳过本地验证节点和合约部署：\r\n```\r\n\u003e anchor test --skip-local-validator --skip-deploy\r\n...\r\n\r\n PASS  tests/favorites.spec.ts (10.049 s)\r\n  test favorites\r\n    ✓ 设置收藏 (202 ms)\r\n\r\nTest Suites: 1 passed, 1 total\r\nTests:       1 passed, 1 total\r\nSnapshots:   0 total\r\nTime:        10.097 s\r\n\r\n```\r\n\r\n这样就完成了合约的测试了。\r\n\r\n## 总结\r\n\r\n\r\n我们在这篇文章中详细介绍了如何使用 Anchor 在本地搭建 Solana 开发环境， 主要是要注意 Rust 工具链的版本问题。\r\n\r\n使用 `create-solana-dapp` 创建新的 Solana DApp 项目，并演示了通过 anchor 命令行工具来编译、部署、测试合约，主要的命令有：\r\n\r\n- 使用 `anchor build` 编译合约，会生成程序二进制文件、keypair、IDL文件。\r\n- 使用 `anchor deploy` 部署合约，会使用 keypair 部署合约，并更新 `Anchor.toml` 和 `lib.rs` 中的 `program id` 。\r\n- 使用 `anchor test` 进行合约测试，测试用例会使用前面的IDL 文件和程序 ID，并使用 `bankrun` 进行合约测试。","title":"Solana 开发系列 3 - 使用 Anchor 开发程序"},"history":null,"timestamp":1737206819,"version":1}