全面指南:使用 React、Anchor、Rust 和 Phantom 进行全栈 Solana 开发

使用 React、Solana、Anchor 和 Phantom 钱包构建全栈 dapp。

全栈以太坊开发完全指南中,我深入探讨了如何在以太坊上构建全栈 dapp,这也适用于其他 EVM 兼容链,如 Polygon、Avalanche 和以太坊 Layer 2,比如 Arbitrum。

在本指南中,我想深入研究 Solana,向你展示如何构建全栈 dapp。我还想向你介绍生态系统和开发工具,希望能帮助你开始构建自己的想法和应用程序。

项目的代码位于此处

Solana 开发者概览

作为学习过 Solidity 及其生态系统的人,我有点认为要开始并不会太难。我错了。

开发工具的部分确实非常好和成熟(Solana CLI 和 Anchor),而生态系统的其他部分,甚至是 Anchor 的文档(公平地说,这是非常新的),还有相当多的不足之处。

尽管如此,一旦你掌握了一切,很快就会更容易理解如何开始实现自己的想法并开始尝试。

寻找答案的关键之一是要在 Google、Github 以及特别是各种 Discord 服务器中(Anchor 和 Solana)进行仔细搜索。这些频道中的开发人员非常乐于助人,尤其是 Armani Ferrante,他创建了 Anchor 框架。熟悉搜索功能,你通常可以在 Discord 中的过去讨论中找到问题的答案。

项目概述

我们今天将使用的工具包括:

Solana 工具套件 - 这包括一个与 Solana 网络交互的非常成熟和文档完善的 CLI。

Anchor 框架 - Anchor 对我来说实际上是一个救命稻草,我几乎可以肯定如果没有它,我将无法克服构建任何东西的难关。它是 Solana 开发的 Hardhat,我喜欢它。它还提供了一个 DSL,使你无需深入了解语言即可开始,尽管我仍在努力学习 Rust,因为即使使用 DSL,构建任何有意义的程序都可能需要会用Rust。学习 Rust 的一个很好的免费地方是 Rust 之书

solana/web3.js - 一个 Solana 版本的 web3.js,看起来运行得相当不错,但对我来说文档几乎无法使用。

React - 客户端框架

我将不会详细介绍 Solana 本身的工作原理,因为其他人可以比我更好地解释这一点。相反,我将尝试专注于构建一些东西并分享你需要了解的细节,以及我认为最重要的事项。

如果你想了解更多关于 Solana 及其工作原理的信息,这里有一些不错的文章:

在本指南中,我们将主要关注项目设置、测试和前端客户端集成,以构建一些类型的应用程序,主要侧重于 CRUD 操作(当然不包括删除),我发现这方面的文档有些不完整(与客户端应用程序的集成)。

我们还将学习如何使用 Solana CLI 向我们自己的开发账户空投代币,并将我们的应用程序部署到本地网络以及实时测试网络。

在本指南中,我们不会专注于 NFT,但也许我会在未来的指南中专注于这一点。如果你有兴趣在 Solana 上构建 NFT 市场,我建议查看 Metaplex

预备条件

本教程涵盖了如何在 Solana 上构建全栈应用程序,但不涉及如何安装所有单独的依赖项。

相反,我将列出依赖项并链接到安装它们的文档,因为每个项目都能更好地解释和记录这些内容,也能保持其更新。

  1. Node.js - 我建议使用 nvmfnm安装 Node

  2. Solana 工具套件 - 你可以查看安装说明此处 。注意 - 如果在 M1 Mac 上安装 Solana 时遇到任何问题,请尝试从源代码构建 ,并查看此指南

  3. Anchor(包括 Mocha 安装) - Anchor 的安装对我来说相当简单。你可以在此处找到安装说明。

  4. Solana 浏览器钱包 - 我推荐使用 Phantom,这是我用来测试此应用程序的钱包。

开始

在开始构建之前,让我们看一下 Solana CLI。

Solana CLI

我们将使用 Solana CLI 主要进行网络配置(在本地主机和开发者测试网络之间)以及向我们的钱包空投代币,其他几乎所有操作都将使用 Anchor CLI。

例如,我们可以使用以下命令检查当前网络(和其他)配置:

solana config get

# 输出
Config File: /Users/user/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/user/.config/solana/id.json
Commitment: confirmed

进入全屏模式退出全屏模式

如果你没有Keypair 路径,请按照此处的说明设置一个。

我们可以这样更改网络:

# 设置为 localhost
solana config set --url localhost

# 设置为 devnet
solana config set --url devnet

进入全屏模式退出全屏模式

这很重要,因为在构建、测试和部署程序时,你需要注意你正在使用的网络,你的钱包在测试时使用的网络必须与你的本地环境使用的网络相同,这是我将要介绍的内容。

我们将从在localhost网络上开发开始,然后切换到devnet网络。我们还可以使用 CLI 查看当前本地钱包地址:

solana address

然后获取关于一个账户的完整详情:

solana account <address from above>

接下来让我们空投一些代币。为此,首先切换到本地网络,因为这是我们将要开始工作的地方:

solana config set --url localhost

接下来,启动本地网络。这将是一个本地的 Solana 节点,我们可以部署用于测试:

solana-test-validator

一旦本地网络运行起来,你可以向你的账户空投代币。在网络运行时,打开一个单独的窗口并运行以下命令:

solana airdrop 100

你可以检查钱包的余额:

solana balance

# 或者

solana balance <address>

你现在应该在你的钱包中有 100 SOL 的余额。有了这个,我们可以开始构建。

让我们开始构建

要开始,初始化一个新的 anchor 项目并切换到新目录:

anchor init mysolanaapp --javascript

cd mysolanaapp

请确保使用 Anchor 版本 0.16.0 或更高版本。

在这个项目中,你会看到四个主要文件夹(除了node_modules):

app - 我们的前端代码将放在这里

programs - 这是 Solana 程序的 Rust 代码所在的地方

test - 程序的 JavaScript 测试所在的地方

migrations - 一个基本的部署脚本

让我们看一下为我们创建的程序。

Anchor 使用并使我们能够编写一个 eDSL( 嵌入式 DSL),它将许多你通常需要做的更复杂的低级操作抽象出来,如果你没有使用它,你将需要使用 Solana 和 Rust 进行更多的操作,这使得它对我来说更容易接近。

// programs/src/lib.rs
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod mysolanaapp {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

这可能是你可以编写的最基本的程序。这里唯一发生的事情是我们定义了一个名为initialize的函数,当调用时,程序会成功退出。这里根本没有数据操作。

Initialize结构定义了上下文为空,没有任何参数。我们将在后面学习更多关于函数上下文的知识。

要编译这个程序,我们可以运行 Anchor 的build命令:

anchor build

一旦构建完成,你应该会看到一个名为target的新文件夹。

创建的工件之一是位于target/idl/mysolanaapp.jsonIDL

IDL 与 Solidity 中的 ABI(或 GraphQL 中的查询定义)非常相似,我们将在 JavaScript 测试和前端中以类似的方式使用它们来通过 RPC 与我们的 Solana 程序通信。

我们也可以测试我们的程序。如果你打开tests/mysolanaapp.js,你会看到其中有一个用 JavaScript 编写的测试,让我们可以测试程序。

测试应该如下所示:

const anchor = require('@project-serum/anchor');

describe('mysolanaapp', () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.Provider.env());

  it('Is initialized!', async () => {
    const program = anchor.workspace.Mysolanaapp;
    const tx = await program.rpc.initialize();
    console.log("Your transaction signature", tx);
  });
});

从这个测试中,有几个重要的东西可以学习,我们将在未来的测试和前端 JavaScript 客户端中使用,这些对我们很重要。

要使用 Anchor 调用 Solana 程序,通常需要两个主要的东西:

1. Provider - Provider是到 Solana 网络的连接的抽象,通常由 Connection、钱包和预提交承诺组成。

在测试中,Anchor 框架将根据环境(anchor.Provider.env())为我们创建提供程序,但在客户端中,我们将需要使用用户的 Solana 钱包构建提供程序。

2. program - program是一个结合了ProvideridlprogramID(在构建程序时生成)的抽象,允许我们对我们的程序调用RPC方法。

Provider一样,Anchor 提供了一种方便的方法来访问program,但在构建前端时,我们需要自己构建这个provider

一旦我们有了这两个东西,我们就可以开始调用程序中的函数。例如,在我们的程序中,我们有一个initialize函数。在我们的测试中,你会看到我们可以直接使用program.rpc.functionName来调用该函数:

const tx = await program.rpc.initialize();

这是你在使用 Anchor 时经常会使用的一种常见模式,一旦你掌握了它的工作原理,就可以很容易地连接到 Solana 程序并与之交互。

我们现在可以通过运行test脚本来测试程序:

anchor test

构建 Hello World


现在我们已经设置好项目,让我们创建一些更有趣的东西。

我知道,作为一个全栈开发人员,大多数时候我在想如何进行 CRUD 类型的操作,所以我们接下来将看看这个。

我们将创建的第一个程序将允许我们创建一个计数器,每次我们从客户端应用程序调用它时都会递增。

我们需要做的第一件事是打开programs/mysolanaapp/src/lib.rs并使用以下代码进行更新:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
mod mysolanaapp {
    use super::*;

    pub fn create(ctx: Context<Create>) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count += 1;
        Ok(())
    }
}

// Transaction instructions
#[derive(Accounts)]
pub struct Create<'info> {
    #[account(init, payer = user, space = 16 + 16)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}
``````rust
// 交易指令
#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

// 一个包含在交易指令中的账户
#[account]
pub struct BaseAccount {
    pub count: u64,
}

在这个程序中,我们有两个函数 - createincrement。这两个函数是我们将能够从客户端应用程序调用的 RPC 请求处理程序,以与程序进行交互。

RPC 处理程序的第一个参数是 Context 结构,描述了在调用函数时将传递的上下文以及如何处理它。在 Create 的情况下,我们期望三个参数:base_accountusersystem_program

#[account(...)] 属性定义了与之前声明的账户相关的约束和指令。如果这些约束中的任何一个不成立,那么指令将永远不会执行。

任何使用正确的 base_account 调用此程序的客户端都可以调用这些 RPC 方法。

Solana 处理数据的方式与我以往接触过的任何东西都不同。程序内部没有持久状态,一切都附加在所谓的账户上。账户基本上包含程序的所有状态。因此,所有数据都是通过引用从外部传递的。

也没有读取操作。这是因为要读取程序的内容,你只需要请求账户,然后你就能查看程序的所有状态。要了解有关账户如何工作的更多信息,请查看此文章

构建程序:

anchor build

接下来,让我们编写一个使用此计数器程序的测试。为此,请打开 tests/mysolanaapp.js 并更新为以下代码:

const assert = require("assert");
const anchor = require("@project-serum/anchor");
const { SystemProgram } = anchor.web3;

describe("mysolanaapp", () => {
  /* 创建并设置提供程序 */
  const provider = anchor.Provider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.Mysolanaapp;
  it("Creates a counter)", async () => {
    /* 通过 RPC 调用 create 函数 */
    const baseAccount = anchor.web3.Keypair.generate();
    await program.rpc.create({
      accounts: {
        baseAccount: baseAccount.publicKey,
        user: provider.wallet.publicKey,
        systemProgram: SystemProgram.programId,
      },
      signers: [baseAccount],
    });

    /* 获取账户并检查 count 的值 */
    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('Count 0: ', account.count.toString())
    assert.ok(account.count.toString() == 0);
    _baseAccount = baseAccount;

  });

  it("Increments the counter", async () => {
    const baseAccount = _baseAccount;

    await program.rpc.increment({
      accounts: {
        baseAccount: baseAccount.publicKey,
      },
    });

    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('Count 1: ', account.count.toString())
    assert.ok(account.count.toString() == 1);
  });
});

在继续测试和部署程序之前,我们要获取由构建生成的动态生成的程序 ID。我们需要此 ID 用于在 Rust 程序中使用,以替换我们在创建项目时设置的占位符 ID。要获取此 ID,可以运行以下命令:

solana address -k target/deploy/mysolanaapp-keypair.json

现在,我们可以在 lib.rs 中更新程序 ID:

// mysolanaapp/src/lib.rs

declare_id!("your-program-id");

以及在 Anchor.toml 中:

# Anchor.toml
[programs.localnet]
mysolanaapp = "your-program-id"

接下来,运行测试:

anchor test

一旦测试通过,我们现在可以部署。

我们现在可以部署程序。确保 solana-test-validator 正在运行:

anchor deploy

你还可以通过打开一个单独的窗口并运行 solana logs 来查看验证器日志

现在我们准备构建前端。

构建 React 应用

在 Anchor 项目的根目录中,创建一个新的 react 应用程序,以覆盖现有的 app 目录:

npx create-react-app app

接下来,安装我们将需要用于 Anchor 和 Solana Web3 的依赖项:

cd app

npm install @project-serum/anchor @solana/web3.js

我们还将使用 Solana Wallet Adapter 来处理连接用户 Solana 钱包。让我们也安装这些依赖项:

npm install @solana/wallet-adapter-react \
@solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets \
@solana/wallet-adapter-base

接下来,在 src 目录中,创建一个名为 idl.json 的新文件。在这里,复制在主项目文件夹中为你创建的 IDL JSON,位于 target/idl/mysolanaapp.json

如果我们可以自动将此 idl 文件复制到我们的客户端应用程序 src 文件夹中,那将是很好的,但目前我还没有找到原生方法来实现这一点。当然,如果你愿意,你可以创建自己的脚本来执行此操作,否则你需要在每次更改主程序后复制并粘贴 IDL。

如果你想要这样的脚本,你可以在几行代码中完成:

// copyIdl.js
const fs = require('fs');
const idl = require('./target/idl/mysolanaapp.json');

fs.writeFileSync('./app/src/idl.json', JSON.stringify(idl));

接下来,打开 app/src/App.js 并更新为以下内容:

import './App.css';
import { useState } from 'react';
import { Connection, PublicKey } from '@solana/web3.js';
import {
  Program, Provider, web3
} from '@project-serum/anchor';
import idl from './idl.json';

import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';
import { useWallet, WalletProvider, ConnectionProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
require('@solana/wallet-adapter-react-ui/styles.css');

const wallets = [
  /* 查看可用钱包列表 https://github.com/solana-labs/wallet-adapter#wallets */
  new PhantomWalletAdapter()
]

const { SystemProgram, Keypair } = web3;
/* 创建一个账户 */
const baseAccount = Keypair.generate();
const opts = {
  preflightCommitment: "processed"
}
const programID = new PublicKey(idl.metadata.address);
``````javascript
function App() {
  const [value, setValue] = useState(null);
  const wallet = useWallet();

  async function getProvider() {
    /* 创建提供程序并将其返回给调用者 */
    /* 网络暂时设置为本地网络 */
    const network = "http://127.0.0.1:8899";
    const connection = new Connection(network, opts.preflightCommitment);

    const provider = new Provider(
      connection, wallet, opts.preflightCommitment,
    );
    return provider;
  }

  async function createCounter() {    
    const provider = await getProvider()
    /* 创建包含 idl、程序 ID 和提供程序的程序接口 */
    const program = new Program(idl, programID, provider);
    try {
      /* 通过 rpc 与程序交互 */
      await program.rpc.create({
        accounts: {
          baseAccount: baseAccount.publicKey,
          user: provider.wallet.publicKey,
          systemProgram: SystemProgram.programId,
        },
        signers: [baseAccount]
      });

      const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
      console.log('account: ', account);
      setValue(account.count.toString());
    } catch (err) {
      console.log("Transaction error: ", err);
    }
  }

  async function increment() {
    const provider = await getProvider();
    const program = new Program(idl, programID, provider);
    await program.rpc.increment({
      accounts: {
        baseAccount: baseAccount.publicKey
      }
    });

    const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
    console.log('account: ', account);
    setValue(account.count.toString());
  }

  if (!wallet.connected) {
    /* 如果用户的钱包未连接,则显示连接钱包按钮。 */
    return (
      <div style={{ display: 'flex', justifyContent: 'center', marginTop:'100px' }}>
        <WalletMultiButton />
      </div>
    )
  } else {
    return (
      <div className="App">
        <div>
          {
            !value && (<button onClick={createCounter}>Create counter</button>)
          }
          {
            value && <button onClick={increment}>Increment counter</button>
          }

          {
            value && value >= Number(0) ? (
              <h2>{value}</h2>
            ) : (
              <h3>Please create the counter.</h3>
            )
          }
        </div>
      </div>
    );
  }
}

/* 钱包配置如下: https://github.com/solana-labs/wallet-adapter#setup */
const AppWithProvider = () => (
  <ConnectionProvider endpoint="http://127.0.0.1:8899">
    <WalletProvider wallets={wallets} autoConnect>
      <WalletModalProvider>
        <App />
      </WalletModalProvider>
    </WalletProvider>
  </ConnectionProvider>
)

export default AppWithProvider;

切换钱包网络

在我们可以与 localhost 网络上的程序交互之前,我们必须将我们的 Phantom 钱包切换到正确的网络。

要这样做,请打开你的 Phantom 钱包并单击设置按钮。然后向下滚动到 更改网络:

Image 1: 更新网络

接下来,选择 Localhost:

Image 2: 选择 localhost

现在我们需要向这个钱包空投代币。在钱包界面顶部,单击你的地址以将其复制到剪贴板。

Image 3: 钱包地址

接下来,打开你的终端并运行以下命令(确保 solana-test-validator 正在运行):

solana airdrop 10 <address>

你现在应该在你的钱包中有 10 个代币。现在,我们可以运行并测试该应用程序!

切换到 app 目录并运行以下命令:

npm start

你应该能够连接你的钱包,创建一个计数器并对其进行递增。

你会注意到,当你刷新页面时,你会丢失程序的状态。这是因为我们在程序加载时动态生成基本帐户。如果你想要在各个客户端之间读取和交互程序数据,你需要在项目中创建并存储 Keypair。我已经整理了一个代码片段 ,展示了这可能是什么样子。

Hello World #2


让我们创建这个程序的一个变体,而不是处理计数器,它允许我们创建一条消息并跟踪所有先前创建的消息。

为此,让我们更新我们的 Rust 程序如下:

/* programs/mysolanaapp/src/lib.rs */
use anchor_lang::prelude::*;

declare_id!("your-program-id");

#[program]
mod mysolanaapp {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, data: String) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        let copy = data.clone();
        base_account.data = data;
        base_account.data_list.push(copy);
        Ok(())
    }

    pub fn update(ctx: Context<Update>, data: String) -> ProgramResult {
        let base_account = &mut ctx.accounts.base_account;
        let copy = data.clone();
        base_account.data = data;
        base_account.data_list.push(copy);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 64 + 64)]
    pub base_account: Account<'info, BaseAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

#[account]
pub struct BaseAccount {
    pub data: String,
    pub data_list: Vec<String>,
}

在这个程序中,我们要跟踪的两个主要数据是一个名为 data 的字符串和一个保存程序中添加的所有数据列表的向量,名为 data_list

你会注意到这里的内存分配比之前的程序要高 (128 + 128),以便考虑到向量。我不知道你能够在这个程序中存储多少更新,但可能值得进一步调查或尝试,因为这个示例本身是实验性的,只是为了让你了解事物是如何工作的。

接下来,我们可以更新这个新程序的测试:

    const assert = require("assert");
    const anchor = require("@project-serum/anchor");
    const { SystemProgram } = anchor.web3;

    describe("Mysolanaapp", () => {
      const provider = anchor.Provider.env();
      anchor.setProvider(provider);
      const program = anchor.workspace.Mysolanaapp;
      it("It initializes the account", async () => {
        const baseAccount = anchor.web3.Keypair.generate();
        await program.rpc.initialize("Hello World", {
          accounts: {
            baseAccount: baseAccount.publicKey,
            user: provider.wallet.publicKey,
            systemProgram: SystemProgram.programId,
          },
          signers: [baseAccount],
        });

        const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
        console.log('Data: ', account.data);
        assert.ok(account.data === "Hello World");
        _baseAccount = baseAccount;

      });

      it("Updates a previously created account", async () => {
        const baseAccount = _baseAccount;

        await program.rpc.update("Some new data", {
          accounts: {
            baseAccount: baseAccount.publicKey,
          },
        });

        const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
        console.log('Updated data: ', account.data)
        assert.ok(account.data === "Some new data");
        console.log('all account data:', account)
        console.log('All data: ', account.dataList);
        assert.ok(account.dataList.length === 2);
      });
    });

要进行测试:

    anchor test

如果测试失败,请尝试关闭验证器,然后再次运行。

接下来,让我们更新客户端。

    /* app/src/App.js */
    import './App.css';
    import { useState } from 'react';
    import { Connection, PublicKey } from '@solana/web3.js';
    import { Program, Provider, web3 } from '@project-serum/anchor';
    import idl from './idl.json';

    import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';
    import { useWallet, WalletProvider, ConnectionProvider } from '@solana/wallet-adapter-react';
    import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
    require('@solana/wallet-adapter-react-ui/styles.css');

    const wallets = [ new PhantomWalletAdapter() ]

    const { SystemProgram, Keypair } = web3;
    const baseAccount = Keypair.generate();
    const opts = {
      preflightCommitment: "processed"
    }
    const programID = new PublicKey(idl.metadata.address);

    function App() {
      const [value, setValue] = useState('');
      const [dataList, setDataList] = useState([]);
      const [input, setInput] = useState('');
      const wallet = useWallet()

      async function getProvider() {
        /* 创建提供程序并将其返回给调用者 */
        /* 网络暂时设置为本地网络 */
        const network = "http://127.0.0.1:8899";
        const connection = new Connection(network, opts.preflightCommitment);

        const provider = new Provider(
          connection, wallet, opts.preflightCommitment,
        );
        return provider;
      }

      async function initialize() {    
        const provider = await getProvider();
        /* 创建将 idl、程序 ID 和提供程序结合在一起的程序接口 */
        const program = new Program(idl, programID, provider);
        try {
          /* 通过 rpc 与程序交互 */
          await program.rpc.initialize("Hello World", {
            accounts: {
              baseAccount: baseAccount.publicKey,
              user: provider.wallet.publicKey,
              systemProgram: SystemProgram.programId,
            },
            signers: [baseAccount]
          });

          const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
          console.log('account: ', account);
          setValue(account.data.toString());
          setDataList(account.dataList);
        } catch (err) {
          console.log("Transaction error: ", err);
        }
      }

      async function update() {
        if (!input) return
        const provider = await getProvider();
        const program = new Program(idl, programID, provider);
        await program.rpc.update(input, {
          accounts: {
            baseAccount: baseAccount.publicKey
          }
        });

        const account = await program.account.baseAccount.fetch(baseAccount.publicKey);
        console.log('account: ', account);
        setValue(account.data.toString());
        setDataList(account.dataList);
        setInput('');
      }

      if (!wallet.connected) {
        return (
          <div style={{ display: 'flex', justifyContent: 'center', marginTop:'100px' }}>
            <WalletMultiButton />
          </div>
        )
      } else {
        return (
          <div className="App">
            <div>
              {
                !value && (<button onClick={initialize}>Initialize</button>)
              }

              {
                value ? (
                  <div>
                    <h2>Current value: {value}</h2>
                    <input
                      placeholder="Add new data"
                      onChange={e => setInput(e.target.value)}
                      value={input}
                    />
                    <button onClick={update}>Add data</button>
                  </div>
                ) : (
                  <h3>Please Inialize.</h3>
                )
              }
              {
                dataList.map((d, i) => <h4 key={i}>{d}</h4>)
              }
            </div>
          </div>
        );
      }
    }

    const AppWithProvider = () => (
      <ConnectionProvider endpoint="http://127.0.0.1:8899">
        <WalletProvider wallets={wallets} autoConnect>
          <WalletModalProvider>
            <App />
          </WalletModalProvider>
        </WalletProvider>
      </ConnectionProvider>
    )

    export default AppWithProvider;

接下来,构建并部署程序(确保 solana-test-validator 正在运行):

    anchor build

    anchor deploy

有了新的构建,你将拥有一个新的 IDL,你需要更新客户端。要么将新的 IDL 复制到 app/src/idl.json,要么运行你的 copyIdl.js 脚本。

测试

在测试新程序时,请确保更新由构建创建的idl.json文件。

切换到app目录并运行start命令:

npm start

部署到 Devnet

从这里开始,向实时网络部署非常简单。我们需要做的主要事情是:

1. 更新 Solana CLI 以使用devnet

solana config set --url devnet

2. 更新 Phantom 钱包以使用devnet

3. 打开Anchor.toml并将集群从localnet更新为devnet

4. 重新构建程序。确保Anchor.toml中的程序 ID 与当前程序 ID 匹配。

5. 再次部署程序,这次将部署到devnet

6.app/src/App.js中,我们还需要更新网络,这次使用@solana/web3中的clusterApiUrl,如下所示:

/* before */
<ConnectionProvider endpoint="http://127.0.0.1:8899">

/* after */
import {
  ...,
  clusterApiUrl
} from '@solana/web3.js';

const network = clusterApiUrl('devnet');

<ConnectionProvider endpoint={network}>

从这里开始,你应该能够像我们之前所做的那样部署和测试。

该项目的代码位于这里

下一步

我建议你查看的另一份深入教程是从头开始创建 Solana dApp,该教程实现了 Twitter 的简化版本作为 Solana dapp。


如果你有兴趣全职从事这样的技术工作,请加入我和我的团队 Edge & Node,我们正在招聘!

本文由 AI 翻译,欢迎小伙伴们来校对

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Nader Dabit
Nader Dabit
Web3 application development.