{"content":{"title":"全面指南：使用 React、Anchor、Rust 和 Phantom 进行全栈 Solana 开发","body":">- 原文链接：https://dev.to/edge-and-node/the-complete-guide-to-full-stack-solana-development-with-react-anchor-rust-and-phantom-3291\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)\r\n>- 本文永久链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/8280)\r\n    \r\n\r\n\r\n在[全栈以太坊开发完全指南](https://learnblockchain.cn/article/2383)中，我深入探讨了如何在以太坊上构建全栈 dapp，这也适用于其他 EVM 兼容链，如 Polygon、Avalanche 和以太坊 Layer 2，比如 Arbitrum。\r\n\r\n在本指南中，我想深入研究 Solana，向你展示如何构建全栈 dapp。我还想向你介绍生态系统和开发工具，希望能帮助你开始构建自己的想法和应用程序。\r\n\r\n> 项目的代码位于[此处](https://github.com/dabit3/complete-guide-to-full-stack-solana-development)\r\n\r\n## Solana 开发者概览\r\n\r\n作为学习过 Solidity 及其生态系统的人，我有点认为要开始并不会太难。我错了。\r\n\r\n开发工具的部分确实非常好和成熟（Solana CLI 和 Anchor），而生态系统的其他部分，甚至是 Anchor 的文档（公平地说，这是非常新的），还有相当多的不足之处。\r\n\r\n尽管如此，一旦你掌握了一切，很快就会更容易理解如何开始实现自己的想法并开始尝试。\r\n\r\n寻找答案的关键之一是要在 Google、Github 以及特别是各种 Discord 服务器中（[Anchor](https://discord.gg/ZCHmqvXgDw) 和 Solana）进行仔细搜索。这些频道中的开发人员非常乐于助人，尤其是 [Armani Ferrante](https://twitter.com/armaniferrante)，他创建了 Anchor 框架。熟悉搜索功能，你通常可以在 Discord 中的过去讨论中找到问题的答案。\r\n\r\n## 项目概述\r\n\r\n我们今天将使用的工具包括：\r\n\r\n[Solana 工具套件](https://docs.solana.com/cli/install-solana-cli-tools) - 这包括一个与 Solana 网络交互的非常成熟和文档完善的 CLI。\r\n\r\n[Anchor 框架](https://project-serum.github.io/anchor/getting-started/introduction.html) - Anchor 对我来说实际上是一个救命稻草，我几乎可以肯定如果没有它，我将无法克服构建任何东西的难关。它是 Solana 开发的 [Hardhat](https://hardhat.org/)，我喜欢它。它还提供了一个 DSL，使你无需深入了解语言即可开始，尽管我仍在努力学习 Rust，因为即使使用 DSL，构建任何有意义的程序都可能需要会用Rust。学习 Rust 的一个很好的免费地方是 [Rust 之书](https://doc.rust-lang.org/book/title-page.html) 。\r\n\r\n[solana/web3.js](https://solana-labs.github.io/solana-web3.js/) - 一个 Solana 版本的 [web3.js](https://web3js.readthedocs.io/)，看起来运行得相当不错，但对我来说文档几乎无法使用。\r\n\r\n[React](https://reactjs.org/) - 客户端框架\r\n\r\n我将不会详细介绍 Solana 本身的工作原理，因为其他人可以比我更好地解释这一点。相反，我将尝试专注于构建一些东西并分享你需要了解的细节，以及我认为最重要的事项。\r\n\r\n如果你想了解更多关于 Solana 及其工作原理的信息，这里有一些不错的文章：\r\n\r\n*   [Solana 文档介绍](https://docs.solana.com/introduction)\r\n    \r\n*   [Solana 到底是怎么回事](https://2501babe.github.io/posts/solana101.html)\r\n    \r\n*   [Solana Summer](https://www.notboring.co/p/solana-summer)\r\n    \r\n\r\n在本指南中，我们将主要关注项目设置、测试和前端客户端集成，以构建一些类型的应用程序，主要侧重于 CRUD 操作（当然不包括删除），我发现这方面的文档有些不完整（与客户端应用程序的集成）。\r\n\r\n我们还将学习如何使用 Solana CLI 向我们自己的开发账户空投代币，并将我们的应用程序部署到本地网络以及实时测试网络。\r\n\r\n在本指南中，我们不会专注于 NFT，但也许我会在未来的指南中专注于这一点。如果你有兴趣在 Solana 上构建 NFT 市场，我建议查看 [Metaplex](https://www.metaplex.com/)。\r\n\r\n## 预备条件\r\n\r\n本教程涵盖了如何在 Solana 上构建全栈应用程序，但不涉及如何安装所有单独的依赖项。\r\n\r\n相反，我将列出依赖项并链接到安装它们的文档，因为每个项目都能更好地解释和记录这些内容，也能保持其更新。\r\n\r\n1.  Node.js - 我建议使用 [nvm](https://github.com/nvm-sh/nvm) 或[fnm](https://github.com/Schniz/fnm)安装 Node\r\n    \r\n2.  Solana 工具套件 - 你可以查看安装说明[此处](https://docs.solana.com/cli/install-solana-cli-tools) 。注意 - 如果在 M1 Mac 上安装 Solana 时遇到任何问题，请尝试[从源代码构建](https://docs.solana.com/cli/install-solana-cli-tools#build-from-source) ，并查看[此指南](https://dev.to/nickgarfield/how-to-install-solana-dev-tools-on-an-m1-mac-kfn) 。\r\n    \r\n3.  Anchor（包括 [Mocha](https://project-serum.github.io/anchor/getting-started/installation.html#install-mocha) 安装） - Anchor 的安装对我来说相当简单。你可以在[此处](https://project-serum.github.io/anchor/getting-started/installation.html)找到安装说明。\r\n    \r\n4.  Solana 浏览器钱包 - 我推荐使用 [Phantom](https://phantom.app/)，这是我用来测试此应用程序的钱包。\r\n    \r\n\r\n## 开始\r\n\r\n在开始构建之前，让我们看一下 Solana CLI。\r\n\r\n###  Solana CLI\r\n\r\n我们将使用 Solana CLI 主要进行网络配置（在本地主机和开发者测试网络之间）以及向我们的钱包空投代币，其他几乎所有操作都将使用 Anchor CLI。\r\n\r\n例如，我们可以使用以下命令检查当前网络（和其他）配置：\r\n\r\n    solana config get\r\n    \r\n    # 输出\r\n    Config File: /Users/user/.config/solana/cli/config.yml\r\n    RPC URL: https://api.devnet.solana.com\r\n    WebSocket URL: wss://api.devnet.solana.com/ (computed)\r\n    Keypair Path: /Users/user/.config/solana/id.json\r\n    Commitment: confirmed\r\n    \r\n\r\n进入全屏模式退出全屏模式\r\n\r\n> 如果你没有`Keypair 路径`，请按照[此处](https://docs.solana.com/wallet-guide/paper-wallet#seed-phrase-generation)的说明设置一个。\r\n\r\n我们可以这样更改网络：\r\n\r\n    # 设置为 localhost\r\n    solana config set --url localhost\r\n    \r\n    # 设置为 devnet\r\n    solana config set --url devnet\r\n    \r\n\r\n进入全屏模式退出全屏模式\r\n\r\n这很重要，因为在构建、测试和部署程序时，你需要注意你正在使用的网络，你的钱包在测试时使用的网络必须与你的本地环境使用的网络相同，这是我将要介绍的内容。\r\n\r\n我们将从在`localhost`网络上开发开始，然后切换到`devnet`网络。我们还可以使用 CLI 查看当前本地钱包地址：\r\n\r\n```shell\r\nsolana address\r\n```\r\n\r\n然后获取关于一个账户的完整详情：\r\n\r\n```shell\r\nsolana account <address from above>\r\n```\r\n\r\n接下来让我们空投一些代币。为此，首先切换到本地网络，因为这是我们将要开始工作的地方：\r\n\r\n```shell\r\nsolana config set --url localhost\r\n```\r\n\r\n接下来，启动本地网络。这将是一个本地的 Solana 节点，我们可以部署用于测试：\r\n\r\n```shell\r\nsolana-test-validator\r\n```\r\n\r\n一旦本地网络运行起来，你可以向你的账户空投代币。在网络运行时，打开一个单独的窗口并运行以下命令：\r\n\r\n```shell\r\nsolana airdrop 100\r\n```\r\n\r\n你可以检查钱包的余额：\r\n\r\n```shell\r\nsolana balance\r\n\r\n# 或者\r\n\r\nsolana balance <address>\r\n```\r\n\r\n你现在应该在你的钱包中有 100 SOL 的余额。有了这个，我们可以开始构建。\r\n\r\n###  让我们开始构建\r\n\r\n要开始，初始化一个新的 anchor 项目并切换到新目录：\r\n\r\n```shell\r\nanchor init mysolanaapp --javascript\r\n\r\ncd mysolanaapp\r\n```\r\n\r\n> 请确保使用 Anchor 版本 0.16.0 或更高版本。\r\n\r\n在这个项目中，你会看到四个主要文件夹（除了**node_modules**）：\r\n\r\n**app** - 我们的前端代码将放在这里\r\n\r\n**programs** - 这是 Solana 程序的 Rust 代码所在的地方\r\n\r\n**test** - 程序的 JavaScript 测试所在的地方\r\n\r\n**migrations** - 一个基本的部署脚本\r\n\r\n让我们看一下为我们创建的程序。\r\n\r\nAnchor 使用并使我们能够编写一个 eDSL（ [嵌入式 DSL](https://en.wikipedia.org/wiki/Domain-specific_language#:~:text=embedded%20domain%2Dspecific%20language%20\\(eDSL,methods%2C%20macros%20etc.\\).)），它将许多你通常需要做的更复杂的低级操作抽象出来，如果你没有使用它，你将需要使用 Solana 和 Rust 进行更多的操作，这使得它对我来说更容易接近。\r\n\r\n```rust\r\n// programs/src/lib.rs\r\nuse anchor_lang::prelude::*;\r\n\r\ndeclare_id!(\"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS\");\r\n\r\n#[program]\r\npub mod mysolanaapp {\r\n    use super::*;\r\n    pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct Initialize {}\r\n```\r\n\r\n这可能是你可以编写的最基本的程序。这里唯一发生的事情是我们定义了一个名为`initialize`的函数，当调用时，程序会成功退出。这里根本没有数据操作。\r\n\r\n`Initialize`结构定义了上下文为空，没有任何参数。我们将在后面学习更多关于函数上下文的知识。\r\n\r\n要编译这个程序，我们可以运行 Anchor 的`build`命令：\r\n\r\n```shell\r\nanchor build\r\n```\r\n\r\n一旦构建完成，你应该会看到一个名为**target**的新文件夹。\r\n\r\n创建的工件之一是位于**target/idl/mysolanaapp.json**的 [IDL](https://en.wikipedia.org/wiki/Interface_description_language)。\r\n\r\nIDL 与 Solidity 中的 [ABI](https://docs.soliditylang.org/en/v0.5.3/abi-spec.html)（或 GraphQL 中的查询定义）非常相似，我们将在 JavaScript 测试和前端中以类似的方式使用它们来通过 RPC 与我们的 Solana 程序通信。\r\n\r\n我们也可以测试我们的程序。如果你打开**tests/mysolanaapp.js**，你会看到其中有一个用 JavaScript 编写的测试，让我们可以测试程序。\r\n\r\n测试应该如下所示：\r\n\r\n```javascript\r\nconst anchor = require('@project-serum/anchor');\r\n\r\ndescribe('mysolanaapp', () => {\r\n  // Configure the client to use the local cluster.\r\n  anchor.setProvider(anchor.Provider.env());\r\n\r\n  it('Is initialized!', async () => {\r\n    const program = anchor.workspace.Mysolanaapp;\r\n    const tx = await program.rpc.initialize();\r\n    console.log(\"Your transaction signature\", tx);\r\n  });\r\n});\r\n```\r\n\r\n从这个测试中，有几个重要的东西可以学习，我们将在未来的测试和前端 JavaScript 客户端中使用，这些对我们很重要。\r\n\r\n要使用 Anchor 调用 Solana 程序，通常需要两个主要的东西：\r\n\r\n**1. `Provider`** - `Provider`是到 Solana 网络的连接的抽象，通常由 [`Connection`](https://solana-labs.github.io/solana-web3.js/classes/Connection.html)、钱包和[预提交承诺](https://solana-labs.github.io/solana-web3.js/modules.html#Commitment)组成。\r\n\r\n在测试中，Anchor 框架将根据环境（`anchor.Provider.env()`）为我们创建提供程序，但在客户端中，我们将需要使用用户的 Solana 钱包构建提供程序。\r\n\r\n**2. `program`** - `program`是一个结合了`Provider`、`idl`和`programID`（在构建程序时生成）的抽象，允许我们对我们的程序调用`RPC`方法。\r\n\r\n与`Provider`一样，Anchor 提供了一种方便的方法来访问`program`，但在构建前端时，我们需要自己构建这个`provider`。\r\n\r\n一旦我们有了这两个东西，我们就可以开始调用程序中的函数。例如，在我们的程序中，我们有一个`initialize`函数。在我们的测试中，你会看到我们可以直接使用`program.rpc.functionName`来调用该函数：\r\n\r\n```javascript\r\nconst tx = await program.rpc.initialize();\r\n```\r\n\r\n这是你在使用 Anchor 时经常会使用的一种常见模式，一旦你掌握了它的工作原理，就可以很容易地连接到 Solana 程序并与之交互。\r\n\r\n我们现在可以通过运行`test`脚本来测试程序：\r\n\r\n```shell\r\nanchor test\r\n```\r\n\r\n## 构建 Hello World\r\n---------------------------------------------\r\n\r\n现在我们已经设置好项目，让我们创建一些更有趣的东西。\r\n\r\n我知道，作为一个全栈开发人员，大多数时候我在想如何进行 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 类型的操作，所以我们接下来将看看这个。\r\n\r\n我们将创建的第一个程序将允许我们创建一个计数器，每次我们从客户端应用程序调用它时都会递增。\r\n\r\n我们需要做的第一件事是打开**programs/mysolanaapp/src/lib.rs**并使用以下代码进行更新： \r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\n\r\ndeclare_id!(\"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS\");\r\n\r\n#[program]\r\nmod mysolanaapp {\r\n    use super::*;\r\n\r\n    pub fn create(ctx: Context<Create>) -> ProgramResult {\r\n        let base_account = &mut ctx.accounts.base_account;\r\n        base_account.count = 0;\r\n        Ok(())\r\n    }\r\n\r\n    pub fn increment(ctx: Context<Increment>) -> ProgramResult {\r\n        let base_account = &mut ctx.accounts.base_account;\r\n        base_account.count += 1;\r\n        Ok(())\r\n    }\r\n}\r\n\r\n// Transaction instructions\r\n#[derive(Accounts)]\r\npub struct Create<'info> {\r\n    #[account(init, payer = user, space = 16 + 16)]\r\n    pub base_account: Account<'info, BaseAccount>,\r\n    #[account(mut)]\r\n    pub user: Signer<'info>,\r\n    pub system_program: Program<'info, System>,\r\n}\r\n``````rust\r\n// 交易指令\r\n#[derive(Accounts)]\r\npub struct Increment<'info> {\r\n    #[account(mut)]\r\n    pub base_account: Account<'info, BaseAccount>,\r\n}\r\n\r\n// 一个包含在交易指令中的账户\r\n#[account]\r\npub struct BaseAccount {\r\n    pub count: u64,\r\n}\r\n```\r\n\r\n在这个程序中，我们有两个函数 - `create` 和 `increment`。这两个函数是我们将能够从客户端应用程序调用的 RPC 请求处理程序，以与程序进行交互。\r\n\r\nRPC 处理程序的第一个参数是 Context 结构，描述了在调用函数时将传递的上下文以及如何处理它。在 `Create` 的情况下，我们期望三个参数：`base_account`、`user` 和 `system_program`。\r\n\r\n`#[account(...)]` 属性定义了与之前声明的账户相关的约束和指令。如果这些约束中的任何一个不成立，那么指令将永远不会执行。\r\n\r\n任何使用正确的 `base_account` 调用此程序的客户端都可以调用这些 RPC 方法。\r\n\r\nSolana 处理数据的方式与我以往接触过的任何东西都不同。程序内部没有持久状态，一切都附加在所谓的账户上。账户基本上包含程序的所有状态。因此，所有数据都是通过引用从外部传递的。\r\n\r\n也没有读取操作。这是因为要读取程序的内容，你只需要请求账户，然后你就能查看程序的所有状态。要了解有关账户如何工作的更多信息，请查看[此文章](https://2501babe.github.io/posts/solana101.html#programs-and-accounts) 。\r\n\r\n构建程序：\r\n\r\n```bash\r\nanchor build\r\n```\r\n\r\n接下来，让我们编写一个使用此计数器程序的测试。为此，请打开 **tests/mysolanaapp.js** 并更新为以下代码：\r\n\r\n```javascript\r\nconst assert = require(\"assert\");\r\nconst anchor = require(\"@project-serum/anchor\");\r\nconst { SystemProgram } = anchor.web3;\r\n\r\ndescribe(\"mysolanaapp\", () => {\r\n  /* 创建并设置提供程序 */\r\n  const provider = anchor.Provider.env();\r\n  anchor.setProvider(provider);\r\n  const program = anchor.workspace.Mysolanaapp;\r\n  it(\"Creates a counter)\", async () => {\r\n    /* 通过 RPC 调用 create 函数 */\r\n    const baseAccount = anchor.web3.Keypair.generate();\r\n    await program.rpc.create({\r\n      accounts: {\r\n        baseAccount: baseAccount.publicKey,\r\n        user: provider.wallet.publicKey,\r\n        systemProgram: SystemProgram.programId,\r\n      },\r\n      signers: [baseAccount],\r\n    });\r\n\r\n    /* 获取账户并检查 count 的值 */\r\n    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n    console.log('Count 0: ', account.count.toString())\r\n    assert.ok(account.count.toString() == 0);\r\n    _baseAccount = baseAccount;\r\n\r\n  });\r\n\r\n  it(\"Increments the counter\", async () => {\r\n    const baseAccount = _baseAccount;\r\n\r\n    await program.rpc.increment({\r\n      accounts: {\r\n        baseAccount: baseAccount.publicKey,\r\n      },\r\n    });\r\n\r\n    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n    console.log('Count 1: ', account.count.toString())\r\n    assert.ok(account.count.toString() == 1);\r\n  });\r\n});\r\n```\r\n\r\n在继续测试和部署程序之前，我们要获取由构建生成的动态生成的程序 ID。我们需要此 ID 用于在 Rust 程序中使用，以替换我们在创建项目时设置的占位符 ID。要获取此 ID，可以运行以下命令：\r\n\r\n```bash\r\nsolana address -k target/deploy/mysolanaapp-keypair.json\r\n```\r\n\r\n现在，我们可以在 **lib.rs** 中更新程序 ID：\r\n\r\n```rust\r\n// mysolanaapp/src/lib.rs\r\n\r\ndeclare_id!(\"your-program-id\");\r\n```\r\n\r\n以及在 **Anchor.toml** 中：\r\n\r\n```toml\r\n# Anchor.toml\r\n[programs.localnet]\r\nmysolanaapp = \"your-program-id\"\r\n```\r\n\r\n接下来，运行测试：\r\n\r\n```bash\r\nanchor test\r\n```\r\n\r\n一旦测试通过，我们现在可以部署。\r\n\r\n我们现在可以部署程序。确保 `solana-test-validator` 正在运行：\r\n\r\n```bash\r\nanchor deploy\r\n```\r\n\r\n> 你还可以通过打开一个单独的窗口并运行 `solana logs` 来查看验证器日志\r\n\r\n现在我们准备构建前端。\r\n\r\n### 构建 React 应用\r\n\r\n在 Anchor 项目的根目录中，创建一个新的 react 应用程序，以覆盖现有的 **app** 目录：\r\n\r\n```bash\r\nnpx create-react-app app\r\n```\r\n\r\n接下来，安装我们将需要用于 Anchor 和 Solana Web3 的依赖项：\r\n\r\n```bash\r\ncd app\r\n\r\nnpm install @project-serum/anchor @solana/web3.js\r\n```\r\n\r\n我们还将使用 [Solana Wallet Adapter](https://github.com/solana-labs/wallet-adapter) 来处理连接用户 Solana 钱包。让我们也安装这些依赖项：\r\n\r\n```bash\r\nnpm install @solana/wallet-adapter-react \\\r\n@solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets \\\r\n@solana/wallet-adapter-base\r\n```\r\n\r\n接下来，在 **src** 目录中，创建一个名为 **idl.json** 的新文件。在这里，复制在主项目文件夹中为你创建的 IDL JSON，位于 **target/idl/mysolanaapp.json**。\r\n\r\n如果我们可以自动将此 **idl** 文件复制到我们的客户端应用程序 **src** 文件夹中，那将是很好的，但目前我还没有找到原生方法来实现这一点。当然，如果你愿意，你可以创建自己的脚本来执行此操作，否则你需要在每次更改主程序后复制并粘贴 IDL。\r\n\r\n如果你想要这样的脚本，你可以在几行代码中完成：\r\n\r\n```javascript\r\n// copyIdl.js\r\nconst fs = require('fs');\r\nconst idl = require('./target/idl/mysolanaapp.json');\r\n\r\nfs.writeFileSync('./app/src/idl.json', JSON.stringify(idl));\r\n```\r\n\r\n接下来，打开 **app/src/App.js** 并更新为以下内容：\r\n\r\n```javascript\r\nimport './App.css';\r\nimport { useState } from 'react';\r\nimport { Connection, PublicKey } from '@solana/web3.js';\r\nimport {\r\n  Program, Provider, web3\r\n} from '@project-serum/anchor';\r\nimport idl from './idl.json';\r\n\r\nimport { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';\r\nimport { useWallet, WalletProvider, ConnectionProvider } from '@solana/wallet-adapter-react';\r\nimport { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';\r\nrequire('@solana/wallet-adapter-react-ui/styles.css');\r\n\r\nconst wallets = [\r\n  /* 查看可用钱包列表 https://github.com/solana-labs/wallet-adapter#wallets */\r\n  new PhantomWalletAdapter()\r\n]\r\n\r\nconst { SystemProgram, Keypair } = web3;\r\n/* 创建一个账户 */\r\nconst baseAccount = Keypair.generate();\r\nconst opts = {\r\n  preflightCommitment: \"processed\"\r\n}\r\nconst programID = new PublicKey(idl.metadata.address);\r\n``````javascript\r\nfunction App() {\r\n  const [value, setValue] = useState(null);\r\n  const wallet = useWallet();\r\n\r\n  async function getProvider() {\r\n    /* 创建提供程序并将其返回给调用者 */\r\n    /* 网络暂时设置为本地网络 */\r\n    const network = \"http://127.0.0.1:8899\";\r\n    const connection = new Connection(network, opts.preflightCommitment);\r\n\r\n    const provider = new Provider(\r\n      connection, wallet, opts.preflightCommitment,\r\n    );\r\n    return provider;\r\n  }\r\n\r\n  async function createCounter() {    \r\n    const provider = await getProvider()\r\n    /* 创建包含 idl、程序 ID 和提供程序的程序接口 */\r\n    const program = new Program(idl, programID, provider);\r\n    try {\r\n      /* 通过 rpc 与程序交互 */\r\n      await program.rpc.create({\r\n        accounts: {\r\n          baseAccount: baseAccount.publicKey,\r\n          user: provider.wallet.publicKey,\r\n          systemProgram: SystemProgram.programId,\r\n        },\r\n        signers: [baseAccount]\r\n      });\r\n\r\n      const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n      console.log('account: ', account);\r\n      setValue(account.count.toString());\r\n    } catch (err) {\r\n      console.log(\"Transaction error: \", err);\r\n    }\r\n  }\r\n\r\n  async function increment() {\r\n    const provider = await getProvider();\r\n    const program = new Program(idl, programID, provider);\r\n    await program.rpc.increment({\r\n      accounts: {\r\n        baseAccount: baseAccount.publicKey\r\n      }\r\n    });\r\n\r\n    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n    console.log('account: ', account);\r\n    setValue(account.count.toString());\r\n  }\r\n\r\n  if (!wallet.connected) {\r\n    /* 如果用户的钱包未连接，则显示连接钱包按钮。 */\r\n    return (\r\n      <div style={{ display: 'flex', justifyContent: 'center', marginTop:'100px' }}>\r\n        <WalletMultiButton />\r\n      </div>\r\n    )\r\n  } else {\r\n    return (\r\n      <div className=\"App\">\r\n        <div>\r\n          {\r\n            !value && (<button onClick={createCounter}>Create counter</button>)\r\n          }\r\n          {\r\n            value && <button onClick={increment}>Increment counter</button>\r\n          }\r\n\r\n          {\r\n            value && value >= Number(0) ? (\r\n              <h2>{value}</h2>\r\n            ) : (\r\n              <h3>Please create the counter.</h3>\r\n            )\r\n          }\r\n        </div>\r\n      </div>\r\n    );\r\n  }\r\n}\r\n\r\n/* 钱包配置如下: https://github.com/solana-labs/wallet-adapter#setup */\r\nconst AppWithProvider = () => (\r\n  <ConnectionProvider endpoint=\"http://127.0.0.1:8899\">\r\n    <WalletProvider wallets={wallets} autoConnect>\r\n      <WalletModalProvider>\r\n        <App />\r\n      </WalletModalProvider>\r\n    </WalletProvider>\r\n  </ConnectionProvider>\r\n)\r\n\r\nexport default AppWithProvider;\r\n```\r\n\r\n###  切换钱包网络\r\n\r\n在我们可以与 `localhost` 网络上的程序交互之前，我们必须将我们的 Phantom 钱包切换到正确的网络。\r\n\r\n要这样做，请打开你的 Phantom 钱包并单击设置按钮。然后向下滚动到 **更改网络**:\r\n\r\n![Image 1: 更新网络](https://img.learnblockchain.cn/attachments/migrate/1717299496839) \r\n\r\n接下来，选择 **Localhost**:\r\n\r\n![Image 2: 选择 localhost](https://img.learnblockchain.cn/attachments/migrate/1717299496840) \r\n\r\n现在我们需要向这个钱包空投代币。在钱包界面顶部，单击你的地址以将其复制到剪贴板。\r\n\r\n![Image 3: 钱包地址](https://img.learnblockchain.cn/attachments/migrate/1717299496841)  \r\n\r\n接下来，打开你的终端并运行以下命令（确保 `solana-test-validator` 正在运行）:\r\n\r\n```bash\r\nsolana airdrop 10 <address>\r\n```\r\n\r\n你现在应该在你的钱包中有 10 个代币。现在，我们可以运行并测试该应用程序！\r\n\r\n切换到 **app** 目录并运行以下命令:\r\n\r\n```bash\r\nnpm start\r\n```\r\n\r\n你应该能够连接你的钱包，创建一个计数器并对其进行递增。\r\n\r\n你会注意到，当你刷新页面时，你会丢失程序的状态。这是因为我们在程序加载时动态生成基本帐户。如果你想要在各个客户端之间读取和交互程序数据，你需要在项目中创建并存储 Keypair。我已经整理了一个[代码片段](https://gist.github.com/dabit3/7cbd18b8bc4b495c4831f8674902eb42) ，展示了这可能是什么样子。\r\n\r\n## Hello World #2\r\n-----------------------------------------\r\n\r\n让我们创建这个程序的一个变体，而不是处理计数器，它允许我们创建一条消息并跟踪所有先前创建的消息。\r\n\r\n为此，让我们更新我们的 Rust 程序如下:\r\n\r\n```rust\r\n/* programs/mysolanaapp/src/lib.rs */\r\nuse anchor_lang::prelude::*;\r\n\r\ndeclare_id!(\"your-program-id\");\r\n\r\n#[program]\r\nmod mysolanaapp {\r\n    use super::*;\r\n\r\n    pub fn initialize(ctx: Context<Initialize>, data: String) -> ProgramResult {\r\n        let base_account = &mut ctx.accounts.base_account;\r\n        let copy = data.clone();\r\n        base_account.data = data;\r\n        base_account.data_list.push(copy);\r\n        Ok(())\r\n    }\r\n\r\n    pub fn update(ctx: Context<Update>, data: String) -> ProgramResult {\r\n        let base_account = &mut ctx.accounts.base_account;\r\n        let copy = data.clone();\r\n        base_account.data = data;\r\n        base_account.data_list.push(copy);\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct Initialize<'info> {\r\n    #[account(init, payer = user, space = 64 + 64)]\r\n    pub base_account: Account<'info, BaseAccount>,\r\n    #[account(mut)]\r\n    pub user: Signer<'info>,\r\n    pub system_program: Program<'info, System>,\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct Update<'info> {\r\n    #[account(mut)]\r\n    pub base_account: Account<'info, BaseAccount>,\r\n}\r\n\r\n\r\n#[account]\r\npub struct BaseAccount {\r\n    pub data: String,\r\n    pub data_list: Vec<String>,\r\n}\r\n```\r\n\r\n在这个程序中，我们要跟踪的两个主要数据是一个名为 `data` 的字符串和一个保存程序中添加的所有数据列表的向量，名为 `data_list`。\r\n\r\n你会注意到这里的内存分配比之前的程序要高 (`128 + 128`)，以便考虑到向量。我不知道你能够在这个程序中存储多少更新，但可能值得进一步调查或尝试，因为这个示例本身是实验性的，只是为了让你了解事物是如何工作的。\r\n\r\n接下来，我们可以更新这个新程序的测试：\r\n\r\n```javascript\r\n    const assert = require(\"assert\");\r\n    const anchor = require(\"@project-serum/anchor\");\r\n    const { SystemProgram } = anchor.web3;\r\n    \r\n    describe(\"Mysolanaapp\", () => {\r\n      const provider = anchor.Provider.env();\r\n      anchor.setProvider(provider);\r\n      const program = anchor.workspace.Mysolanaapp;\r\n      it(\"It initializes the account\", async () => {\r\n        const baseAccount = anchor.web3.Keypair.generate();\r\n        await program.rpc.initialize(\"Hello World\", {\r\n          accounts: {\r\n            baseAccount: baseAccount.publicKey,\r\n            user: provider.wallet.publicKey,\r\n            systemProgram: SystemProgram.programId,\r\n          },\r\n          signers: [baseAccount],\r\n        });\r\n    \r\n        const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n        console.log('Data: ', account.data);\r\n        assert.ok(account.data === \"Hello World\");\r\n        _baseAccount = baseAccount;\r\n    \r\n      });\r\n    \r\n      it(\"Updates a previously created account\", async () => {\r\n        const baseAccount = _baseAccount;\r\n    \r\n        await program.rpc.update(\"Some new data\", {\r\n          accounts: {\r\n            baseAccount: baseAccount.publicKey,\r\n          },\r\n        });\r\n    \r\n        const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n        console.log('Updated data: ', account.data)\r\n        assert.ok(account.data === \"Some new data\");\r\n        console.log('all account data:', account)\r\n        console.log('All data: ', account.dataList);\r\n        assert.ok(account.dataList.length === 2);\r\n      });\r\n    });\r\n```\r\n\r\n要进行测试：\r\n\r\n```bash\r\n    anchor test\r\n```\r\n\r\n> 如果测试失败，请尝试关闭验证器，然后再次运行。\r\n\r\n接下来，让我们更新客户端。\r\n\r\n```javascript\r\n    /* app/src/App.js */\r\n    import './App.css';\r\n    import { useState } from 'react';\r\n    import { Connection, PublicKey } from '@solana/web3.js';\r\n    import { Program, Provider, web3 } from '@project-serum/anchor';\r\n    import idl from './idl.json';\r\n    \r\n    import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';\r\n    import { useWallet, WalletProvider, ConnectionProvider } from '@solana/wallet-adapter-react';\r\n    import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';\r\n    require('@solana/wallet-adapter-react-ui/styles.css');\r\n    \r\n    const wallets = [ new PhantomWalletAdapter() ]\r\n    \r\n    const { SystemProgram, Keypair } = web3;\r\n    const baseAccount = Keypair.generate();\r\n    const opts = {\r\n      preflightCommitment: \"processed\"\r\n    }\r\n    const programID = new PublicKey(idl.metadata.address);\r\n    \r\n    function App() {\r\n      const [value, setValue] = useState('');\r\n      const [dataList, setDataList] = useState([]);\r\n      const [input, setInput] = useState('');\r\n      const wallet = useWallet()\r\n    \r\n      async function getProvider() {\r\n        /* 创建提供程序并将其返回给调用者 */\r\n        /* 网络暂时设置为本地网络 */\r\n        const network = \"http://127.0.0.1:8899\";\r\n        const connection = new Connection(network, opts.preflightCommitment);\r\n    \r\n        const provider = new Provider(\r\n          connection, wallet, opts.preflightCommitment,\r\n        );\r\n        return provider;\r\n      }\r\n    \r\n      async function initialize() {    \r\n        const provider = await getProvider();\r\n        /* 创建将 idl、程序 ID 和提供程序结合在一起的程序接口 */\r\n        const program = new Program(idl, programID, provider);\r\n        try {\r\n          /* 通过 rpc 与程序交互 */\r\n          await program.rpc.initialize(\"Hello World\", {\r\n            accounts: {\r\n              baseAccount: baseAccount.publicKey,\r\n              user: provider.wallet.publicKey,\r\n              systemProgram: SystemProgram.programId,\r\n            },\r\n            signers: [baseAccount]\r\n          });\r\n    \r\n          const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n          console.log('account: ', account);\r\n          setValue(account.data.toString());\r\n          setDataList(account.dataList);\r\n        } catch (err) {\r\n          console.log(\"Transaction error: \", err);\r\n        }\r\n      }\r\n    \r\n      async function update() {\r\n        if (!input) return\r\n        const provider = await getProvider();\r\n        const program = new Program(idl, programID, provider);\r\n        await program.rpc.update(input, {\r\n          accounts: {\r\n            baseAccount: baseAccount.publicKey\r\n          }\r\n        });\r\n    \r\n        const account = await program.account.baseAccount.fetch(baseAccount.publicKey);\r\n        console.log('account: ', account);\r\n        setValue(account.data.toString());\r\n        setDataList(account.dataList);\r\n        setInput('');\r\n      }\r\n    \r\n      if (!wallet.connected) {\r\n        return (\r\n          <div style={{ display: 'flex', justifyContent: 'center', marginTop:'100px' }}>\r\n            <WalletMultiButton />\r\n          </div>\r\n        )\r\n      } else {\r\n        return (\r\n          <div className=\"App\">\r\n            <div>\r\n              {\r\n                !value && (<button onClick={initialize}>Initialize</button>)\r\n              }\r\n    \r\n              {\r\n                value ? (\r\n                  <div>\r\n                    <h2>Current value: {value}</h2>\r\n                    <input\r\n                      placeholder=\"Add new data\"\r\n                      onChange={e => setInput(e.target.value)}\r\n                      value={input}\r\n                    />\r\n                    <button onClick={update}>Add data</button>\r\n                  </div>\r\n                ) : (\r\n                  <h3>Please Inialize.</h3>\r\n                )\r\n              }\r\n              {\r\n                dataList.map((d, i) => <h4 key={i}>{d}</h4>)\r\n              }\r\n            </div>\r\n          </div>\r\n        );\r\n      }\r\n    }\r\n    \r\n    const AppWithProvider = () => (\r\n      <ConnectionProvider endpoint=\"http://127.0.0.1:8899\">\r\n        <WalletProvider wallets={wallets} autoConnect>\r\n          <WalletModalProvider>\r\n            <App />\r\n          </WalletModalProvider>\r\n        </WalletProvider>\r\n      </ConnectionProvider>\r\n    )\r\n    \r\n    export default AppWithProvider;\r\n```\r\n\r\n接下来，构建并部署程序（确保 `solana-test-validator` 正在运行）：\r\n\r\n```bash\r\n    anchor build\r\n    \r\n    anchor deploy\r\n```\r\n\r\n有了新的构建，你将拥有一个新的 **IDL**，你需要更新客户端。要么将新的 IDL 复制到 **app/src/idl.json**，要么运行你的 **copyIdl.js** 脚本。\r\n\r\n### 测试 \r\n在测试新程序时，请确保更新由构建创建的**idl.json**文件。\r\n\r\n切换到**app**目录并运行`start`命令：\r\n\r\n```bash\r\nnpm start\r\n```\r\n\r\n\r\n### 部署到 Devnet\r\n\r\n\r\n从这里开始，向实时网络部署非常简单。我们需要做的主要事情是：\r\n\r\n**1.** 更新 Solana CLI 以使用`devnet`：\r\n\r\n```bash\r\nsolana config set --url devnet\r\n```\r\n\r\n**2.** 更新 Phantom 钱包以使用`devnet`\r\n\r\n**3.** 打开**Anchor.toml**并将集群从`localnet`更新为`devnet`。\r\n\r\n**4.** 重新构建程序。确保**Anchor.toml**中的程序 ID 与当前程序 ID 匹配。\r\n\r\n**5.** 再次部署程序，这次将部署到`devnet`\r\n\r\n**6.** 在**app/src/App.js**中，我们还需要更新网络，这次使用`@solana/web3`中的`clusterApiUrl`，如下所示：\r\n\r\n```javascript\r\n/* before */\r\n<ConnectionProvider endpoint=\"http://127.0.0.1:8899\">\r\n\r\n/* after */\r\nimport {\r\n  ...,\r\n  clusterApiUrl\r\n} from '@solana/web3.js';\r\n\r\nconst network = clusterApiUrl('devnet');\r\n\r\n<ConnectionProvider endpoint={network}>\r\n```\r\n\r\n从这里开始，你应该能够像我们之前所做的那样部署和测试。\r\n\r\n> 该项目的代码位于[这里](https://github.com/dabit3/complete-guide-to-full-stack-solana-development)\r\n\r\n### 下一步\r\n\r\n我建议你查看的另一份深入教程是[从头开始创建 Solana dApp](https://lorisleiva.com/create-a-solana-dapp-from-scratch)，该教程实现了 Twitter 的简化版本作为 Solana dapp。\r\n\r\n* * *\r\n\r\n如果你有兴趣全职从事这样的技术工作，请加入我和我的团队 [Edge & Node](https://edgeandnode.com/jobs)，我们正在招聘！\r\n\r\n> 本文由 AI 翻译，欢迎小伙伴们来[校对](https://github.com/lbc-team/Pioneer/blob/master/translations/8280.md)。"},"author":{"user":"https://learnblockchain.cn/people/20701","address":null},"history":null,"timestamp":1717300521,"version":1}