使用 React、Anchor、Rust 和 Phantom 进行全栈 Solana 开发的完整指南

2025-05-24

使用 React、Anchor、Rust 和 Phantom 进行全栈 Solana 开发的完整指南

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

在《全栈以太坊开发完整指南》中,我深入研究了如何在以太坊上构建全栈 dapp,它也可以应用于其他 EVM 兼容链,如 Polygon、Avalanche 和以太坊第 2 层,如 Arbitrum。

在本指南中,我将深入探讨 Solana,向您展示如何构建一个全栈 dapp。我还想向您介绍它的生态系统和开发者工具,希望能帮助您快速上手,构建属于您自己的想法和应用程序。

该项目的代码位于此处

Solana 开发人员概览

作为一个大约六个月前才开始学习 Solidity 及其生态系统的人,我以为它的上手和运行应该不会太难。但我错了。

部分开发人员工具确实非常出色且完善(Solana CLI 和 Anchor),而其余生态系统,甚至 Anchor 的文档(公平地说,非常新),都还有不少不足之处。

话虽如此,一旦你掌握了一切,你就会很快更容易理解如何开始实施自己的想法并开始实验。

找到答案的关键之一是要时刻警惕地在 Google、Github,尤其是Anchor和 Solana的各种 Discord 服务器上进行搜索。这些频道的开发者们给予了极大的帮助,尤其是创建了 Anchor 框架的Armani Ferrante。熟悉搜索功能,你通常可以在 Discord 上过去的讨论中找到问题的答案。

项目概述

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

Solana 工具套件——其中包括一个非常精致且有详细文档的 CLI,用于与 Solana 网络交互。

Anchor 框架- Anchor 对我来说简直是救星,如果没有它,我几乎可以肯定,我根本无法克服构建任何代码的困难。它是Solana 开发的“安全帽”,甚至更多,我非常喜欢它。它还在 Rust 之上提供了 DSL,因此你无需深入了解该语言即可上手。不过,我仍在尝试学习 Rust,因为即使使用 DSL,它也可能对构建任何复杂的东西有所帮助。Rust Book是一个不错的免费学习 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 命令行界面

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

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



solana config get

# output
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


Enter fullscreen mode Exit fullscreen mode

如果您没有,请按照此处的Keypair path说明进行设置

我们可以像这样改变网络:



# set to localhost
solana config set --url localhost

# set to devnet
solana config set --url devnet


Enter fullscreen mode Exit fullscreen mode

这一点很重要,因为在构建、测试和部署程序时,你需要了解所使用的网络。你还需要确保你的钱包使用的网络与本地环境测试时使用的网络相同,我将在后面详细介绍。

我们将从在localhost网络上进行开发开始,然后切换到devnet网络。

我们还可以使用 CLI 查看我们当前的本地钱包地址:



solana address


Enter fullscreen mode Exit fullscreen mode

然后获取有关帐户的完整详细信息:



solana account <address from above>


Enter fullscreen mode Exit fullscreen mode

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



solana config set --url localhost


Enter fullscreen mode Exit fullscreen mode

接下来,启动本地网络。这将是一个本地 Solana 节点,我们可以将其部署到该节点进行测试:



solana-test-validator


Enter fullscreen mode Exit fullscreen mode

本地网络运行后,您就可以将代币空投到您的帐户。在网络运行的情况下,打开一个单独的窗口并运行以下命令:



solana airdrop 100


Enter fullscreen mode Exit fullscreen mode

您可以检查钱包余额:



solana balance

# or

solana balance <address>


Enter fullscreen mode Exit fullscreen mode

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

让我们开始构建

首先,初始化一个新的锚点项目并切换到新目录:



anchor init mysolanaapp --javascript

cd mysolanaapp


Enter fullscreen mode Exit fullscreen mode

请务必使用 Anchor 0.16.0 或更高版本。

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

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

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

测试- 程序的 JavaScript 测试现场

migrations - 一个基本的部署脚本

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

Anchor 使用 eDSL(嵌入式 DSL ) ,并使我们能够编写 eDSL,它抽象出许多更复杂的低级操作,如果您在没有它的情况下使用 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 {}


Enter fullscreen mode Exit fullscreen mode

这可能是你能写出的最基本的程序了。这里唯一需要注意的是,我们定义了一个名为 的函数initialize,调用该函数时会成功退出程序。程序中没有任何数据操作。

Initialize结构体将上下文定义为不包含任何参数。稍后我们将详细了解函数上下文。

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



anchor build


Enter fullscreen mode Exit fullscreen mode

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

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

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);
  });
});


Enter fullscreen mode Exit fullscreen mode

从这次测试中我们可以学到一些重要的东西,我们将在将来的测试中以及在前端 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();


Enter fullscreen mode Exit fullscreen mode

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

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



anchor test


Enter fullscreen mode Exit fullscreen mode

构建 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>,
}

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

// An account that goes inside a transaction instruction
#[account]
pub struct BaseAccount {
    pub count: u64,
}


Enter fullscreen mode Exit fullscreen mode

在这个程序中,我们有两个函数——createincrement。这两个函数是 RPC 请求处理程序,我们可以从客户端应用程序调用它们来与程序进行交互。

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

这些#[account(...)]属性定义了与声明的后续帐户相关的约束和指令。如果任何约束不成立,则该指令将永远不会执行。

任何以适当的方式调用该程序的客户端都base_account可以调用这些 RPC 方法。

Solana 处理数据的方式与我以往接触过的任何系统都截然不同。程序内部没有持久化状态,所有内容都与所谓的账户关联。账户本质上保存了程序的所有状态。因此,所有数据都是通过引用从外部传递的。

也无需读取操作。这是因为,要读取程序内容,您只需请求账户,然后就能查看程序的所有状态。想了解更多关于账户工作原理的信息,请查看这篇文章

要构建程序:



anchor build


Enter fullscreen mode Exit fullscreen mode

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



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

describe("mysolanaapp", () => {
  /* create and set a Provider */
  const provider = anchor.Provider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.Mysolanaapp;
  it("Creates a counter)", async () => {
    /* Call the create function via RPC */
    const baseAccount = anchor.web3.Keypair.generate();
    await program.rpc.create({
      accounts: {
        baseAccount: baseAccount.publicKey,
        user: provider.wallet.publicKey,
        systemProgram: SystemProgram.programId,
      },
      signers: [baseAccount],
    });

    /* Fetch the account and check the value of 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);
  });
});


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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



// mysolanaapp/src/lib.rs

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


Enter fullscreen mode Exit fullscreen mode

Anchor.toml中:



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


Enter fullscreen mode Exit fullscreen mode

接下来运行测试:



anchor test


Enter fullscreen mode Exit fullscreen mode

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

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



anchor deploy


Enter fullscreen mode Exit fullscreen mode

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

现在我们准备构建前端。

构建 React 应用

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



npx create-react-app app


Enter fullscreen mode Exit fullscreen mode

接下来,安装 Anchor 和 Solana Web3 所需的依赖项:



cd app

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


Enter fullscreen mode Exit fullscreen mode

我们还将使用Solana 钱包适配器来处理用户 Solana 钱包的连接。让我们也安装以下依赖项:



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


Enter fullscreen mode Exit fullscreen mode

接下来,在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));


Enter fullscreen mode Exit fullscreen mode

接下来,打开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 = [
  /* view list of available wallets at https://github.com/solana-labs/wallet-adapter#wallets */
  new PhantomWalletAdapter()
]

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

function App() {
  const [value, setValue] = useState(null);
  const wallet = useWallet();

  async function getProvider() {
    /* create the provider and return it to the caller */
    /* network set to local network for now */
    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()
    /* create the program interface combining the idl, program ID, and provider */
    const program = new Program(idl, programID, provider);
    try {
      /* interact with the program via 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) {
    /* If the user's wallet is not connected, display connect wallet button. */
    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>
    );
  }
}

/* wallet configuration as specified here: 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;


Enter fullscreen mode Exit fullscreen mode

切换你的钱包网络

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

打开你的 Phantom 钱包,点击“设置”按钮。然后向下滚动到“更改网络”

正在更新网络

接下来,选择Localhost

选择本地主机

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

钱包地址

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



solana airdrop 10 <address>


Enter fullscreen mode Exit fullscreen mode

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

进入应用程序目录并运行以下命令:



npm start


Enter fullscreen mode Exit fullscreen mode

您应该能够连接您的钱包,创建一个计数器,并增加它。

您会注意到,刷新后程序状态会丢失。这是因为我们在程序加载时动态生成了基础账户。如果您想跨不同客户端读取和交互程序数据,则需要在项目中的某个位置创建并存储密钥对。我整理了一个大概的实现方法。

你好,世界第 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>,
}


Enter fullscreen mode Exit fullscreen mode

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

您会注意到,此处的内存分配128 + 128比上一个程序更高( ),以便计算 Vector 的值。我不知道您能在这个程序中存储多少更新,但可能需要进一步研究或进行实验,因为这个示例本身是实验性的,只是为了让您了解其工作原理。

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



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);
  });
});


Enter fullscreen mode Exit fullscreen mode

测试一下:



anchor test


Enter fullscreen mode Exit fullscreen mode

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

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



/* 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() {
    /* create the provider and return it to the caller */
    /* network set to local network for now */
    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();
    /* create the program interface combining the idl, program ID, and provider */
    const program = new Program(idl, programID, provider);
    try {
      /* interact with the program via 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;    


Enter fullscreen mode Exit fullscreen mode

接下来,构建并部署程序(确保solana-test-validator is running):



anchor build

anchor deploy


Enter fullscreen mode Exit fullscreen mode

新版本会生成一个新的IDL,您需要为客户端更新它。您可以将新的 IDL 复制到app/src/idl.json或运行copyIdl.js脚本。

测试一下

测试新程序时,请务必更新构建时创建的idl.json文件。

进入应用程序目录并运行start命令:



npm start


Enter fullscreen mode Exit fullscreen mode

部署到 Devnet

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

1.更新 Solana CLI 以使用devnet



solana config set --url devnet


Enter fullscreen mode Exit fullscreen mode

2.更新 Phantom 钱包以便使用devnet

3.打开Anchor.toml并将集群从 更新localnetdevnet

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

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

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



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

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

const network = clusterApiUrl('devnet');

<ConnectionProvider endpoint={network}>


Enter fullscreen mode Exit fullscreen mode

从这里开始,您应该能够按照我们前面的步骤进行部署和测试。

该项目的代码位于此处

后续步骤Next steps

我建议接下来查看的另一个深入教程是从头开始创建 Solana dApp,它将 Twitter 的简化版本实现为 Solana dapp。


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

文章来源:https://dev.to/edge-and-node/the-complete-guide-to-full-stack-solana-development-with-react-anchor-rust-and-phantom-3291
PREV
全栈 Web3 开发完整指南
NEXT
如何构建全栈 NFT 市场 - V2(2022 年)