Solana Web3 技术栈 - 开发者指南

作为一个开发者如何开始在Solana上构建dapp

简介

在这篇博客中,我们讨论一下Solana区块链,以及作为一个开发者如何开始在Solana上构建dapp。写这篇文章时,我们考虑到了新的开发者和初学者,他们对智能合约和dapps仅有一点的了解。我们将探讨一些高层次的概念、工具和技术,这些都是Solana开发所需要的,最后我们将建立一个小的dapp。如果这让你感到兴奋,那就加入享受吧!

开始

Solana是一个高性能的区块链,提供高吞吐量和非常低的Gas费用。它通过其历史证明机制实现了这一点,该机制被用来提高其POS共识机制的性能。

现在,谈及在Solana上的开发,有一定的优点和缺点。优点是,像Solana CLI、Anchor CLI这样的开发者工具以及它们的SDK都很不错,而且很容易理解和实现。但是,由于生态系统和这些工具都是非常新的,文档并不完善,缺乏必要的解释。

不过,Solana的开发者社区非常强大,人们会热衷于帮助另一个开发者伙伴。强烈建议加入SolanaAnchor Discord,以了解生态系统的最新变化。此外,如果你在Solana开发过程中遇到任何技术问题,一个解决你问题的好地方是Solana Stack Exchange

Solana Web3技术栈

Solana有一个非常好的工具生态系统和技术栈。让我们看看开发程序需要和使用的工具:

1. Solana工具套件

Solana Tool Suite带有Solana CLI工具,它使开发过程变得顺利和简单。你可以用CLI工具执行很多任务,从部署Solana程序到将SPL代币转账到另一个账户。

这里下载工具套件。

2. Rust

Solana智能合约(称为Programs)可以用C、C++或Rust编程语言编写。但最喜欢的是Rust。

Rust是一种底层的编程语言,由于其强调性能,以及类型和内存安全,已经获得了很多人的青睐。

Rust一开始会让人觉得有点害怕,但一旦你开始掌握它,你会非常喜欢它。它有一个非常好的文档,它也可以作为一个很好的学习资源。其他一些关于Rust的资源包括RustlingsRust-By-Example

你可以在这里安装Rust。

3. Anchor

Anchor是Solana的Sealevel运行时的一个框架,为编写智能合约提供了几个方便的开发者工具。Anchor通过处理大量的模板代码使我们的开发变得更加轻松,这样我们就可以专注于重要部分。它还代表我们做了很多检查,使Solana程序保持安全。

Anchor Book是Anchor当前的文档,对于使用Anchor编写Solana程序有很好的参考价值。Anchor SDK typedoc有你可以在JS客户端使用的所有方法、接口和类。该SDK确实需要更好的文档。

你可以在这里安装Anchor 。

4. 前端框架

为了让你的用户使用dapp,你需要有一个能够与区块链通信的前端。你可以用任何一个常见的框架(React / Vue / Angular)编写你的客户端逻 辑。

如果你想用这些框架构建你的客户端,你需要在你的系统中安装NodeJS。你可以在这里安装它。

建立一个Solana Dapp

现在,我们对Solana的开发工作流程有了一个了解,让我们来建立一个Solana Dapp,我们从建立一个简单的计数器应用程序开始行动吧!

设置环境

在构建dapp之前,需要先确保我们需要的工具已经成功安装。需要在你的系统中安装rust、anchor和solana。

注意:如果你在windows上,你将需要一个WSL终端来运行Solana。Solana不能与Powershell很好地工作。

打开你的终端,运行这些命令:

$ rustc --version
rustc 1.63.0-nightly

$ anchor --version
anchor-cli 0.25.0

$ solana --version
solana-cli 1.10.28

如果你正确得到了的版本,这意味着工具被正确安装。

现在运行这个命令:

$ solana-test-validator

Ledger location: test-ledger 
Log: test-ledger/validator.log 

Initializing... 

Version: 1.10.28 
Shred Version: 483 
Gossip Address: 127.0.0.1:1024 
TPU Address: 127.0.0.1:1027 
JSON RPC URL: http://127.0.0.1:8899 
00:00:16 | Processed Slot: 12 | Confirmed Slot: 12 | Finalized Slot: 0 | Full Snapshot Slot: - | Incremental Snapshot Slot: - | Transactions: 11 | ◎499.999945000

如果你的终端输出是这样的,这意味着测试验证器已经在你的系统上成功运行了,你已经准备好开始构建了!

现在,如果有什么地方出错了,你不必惊慌。只要退后一步,重新安装就可以了。

计数器程序概述

在写代码之前,让我们退一步,讨论一下我们的计数器程序需要哪些功能。应该有一个函数来初始化计数器,有一个函数来进行递增,还有另一个函数来进行递减

现在你应该知道的第一件事是,Solana程序并不存储状态,需要存储一个程序的状态,你需要初始化一个叫做账户(account)的东西。基本上有三种类型的账户:

  1. 程序账户:存储可执行代码的账户,是合约被部署的地方。
  2. 存储账户:存储数据的账户,通常情况下,它们存储一个程序的状态。
  3. 代币账户: 储存不同SPL代币余额的账户,以及代币转账的地方。

在我们正在建立的计数器程序中,我们的可执行代码将被存储在程序账户中,而我们的计数器数据将被存储在存储账户

我希望你能明白,如果没有,请不要担心。它最终会变得很直观。好了,让我们继续前进吧!

建立计数器程序

让我们最终开始建立程序吧! 打开终端并运行:

$ anchor init counter

这将初始化一个有几个文件的模板程序。在这里介绍一下重要的文件:

在你项目的根目录下,你会发现文件Anchor.toml,它将包含程序的工作区范围内的配置。

文件programs/counter/src/lib.rs将包含计数器程序的源代码。这是大部分逻辑将被放在这里,里面已经有了一些示例代码。

文件programs/counter/Cargo.toml将包含计数器程序的package/lib/features/dependencies/的信息。

最后但并非最不重要的是,在tests目录下,有程序所需的所有测试。测试对于智能合约的开发是非常关键的,因为我们无法承受其中的漏洞。

现在,让我们运行anchor build,对包含计数器程序进行构建。它将在./target/idl/counter.json下创建一个IDL(接口描述语言)。IDL为我们提供了一个接口,在我们的程序被部署到链上后,任何客户端都可以与之交互。

$ anchor build

运行anchor build将显示一个警告,但你现在可以忽略它。

现在打开lib.rs,删除一些示例代码,使其看起来像这样:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;
}

让我们看一下这里的内容。在use anchor_lang::prelude::*; 一行中,我们所做的是导入anchor_lang 下 prelude 模块中的所有内容。在你使用anchor-lang编写的任何程序中,你都需要有这一行。

接下来,declare_id!(Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS);让我们拥有Solana程序的唯一ID。文Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS是默认的,我们将在稍后改变它。

#[program]是一个属性,用来定义模块, 模块包含所有指令处理程序(handlers,也即我们编写的函数),他们定义了进入Solana程序的所有入口。

很好,现在我们明白了这一切是什么,让我们来编写一下将进入交易指令中的账户,lib.rs应该看起来像这样:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;
}

// --new change--
#[account]
pub struct BaseAccount {
    pub count: u64,
}

#[account]是一个数据结构的属性,代表Solana账户。这里创建了一个名为BaseAccount的结构体,它将count状态存储为一个64位无符号整数。这就是计数将被存储的地方。BaseAccount本质上是我们的存储账户

很好! 现在让我们看看初始化BaseAccount的交易指令。

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;
}

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

#[account]
pub struct BaseAccount {
    pub count: u64,
}

在这里,我们创建了一个名为Initialize的结构体,在这里我们声明了这个交易需要的所有账户,让我们一个一个的看。

  1. base_account: 要初始化base_account,我们的指令中需要有这个账户(显然)。在account属性中,我们传入3个参数。init声明我们正在初始化账户。现在可能会想到的一件事是,如果它还没有被初始化,我们如何在指令中传递baseAccount。我们可以这样做的原因是,之后在写测试时也会看到,我们将为baseAccount的创建并传递一个密钥对。只有在指令成功发生后,baseAccount账户才会在Solana链上为我们创建的密钥对创建。 payer声明了将付费创建账户的用户。这里需要注意的是,在链上存储数据不是免费的。它需要花费SOL。在这种情况下,user账户将支付租金来初始化base_accountspace表示我们需要给账户的空间数量。8个字节用于一个独一无二的discriminator,16个字节用于计数数据。
  2. 用户user是授权者,其有权签署初始化base_account的交易。
  3. system_programsystem_program是Solana上的一个本地程序,负责创建账户,存储账户上的数据,并将账户的所有权分配给连接的程序。每当我们想初始化一个账户时,都需要在交易指令中传递它。

很好! 现在让我们编写处理函数,它将初始化base_account:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    // --new change--
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }
}

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

#[account]
pub struct BaseAccount {
    pub count: u64,
}

counter模块中,编写了一个initialize函数,它将Initialize账户结构体作为上下文。在我们的函数中,我们所做的就是,获取一个base_account的可变引用,并将base_account的计数设为0,就这么简单。

很好! 我们已经成功地写出了初始化链上的base_account的逻辑,它将存储count计数。

计数器递增

让我们添加逻辑来递增计数器,添加交易指令结构体以实现递增。

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }
}

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

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

#[account]
pub struct BaseAccount {
    pub count: u64,
}

我们的交易指令中唯一需要计数器递增的账户是base_account

让我们添加increment处理函数:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }

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

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

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

#[account]
pub struct BaseAccount {
    pub count: u64,
}

我们在这里所做的就是获取一个base_account的可变引用,并将其递增1。够简单了!

很好! 我们现在有逻辑来递增计数器。

计数器递减

这段代码将与增加计数器的代码非常相似:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count = 0;
        Ok(())
    }

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

    // --new change--
    pub fn decrement(ctx: Context<Decrement>) -> Result<()> {
        let base_account = &mut ctx.accounts.base_account;
        base_account.count -= 1;
        Ok(())
    }
}

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

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

// --new change--
#[derive(Accounts)]
pub struct Decrement<'info> {
    #[account(mut)]
    pub base_account: Account<'info, BaseAccount>,
}

#[account]
pub struct BaseAccount {
    pub count: u64,
}

作为最后一步,让我们再一次构建,以检查我们的代码在编译时是否没有错误。

$ anchor build

很好! 我们现在有了我们的计数器程序的智能合约!

测试我们的程序

正确测试我们的智能合约是非常关键的,这样程序就不容易有漏洞。对于计数器程序,我们将实现基本测试,以检查处理程序是否正常工作。

转到tests/counter.ts去吧。我们将把所有的测试放在这里。以这种方式修改测试文件:

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();
}

代码在为baseAccount生成一个新的密钥对,我们将在测试中使用。

测试初始化计数器

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();

    // -- new changes --
    it("initializes the counter", async () => {
        await program.methods
            .initialize()
            .accounts({
                baseAccount: baseAccount.publicKey,
                user: provider.wallet.publicKey,
                systemProgram: anchor.web3.SystemProgram.programId,
            })
            .signers([baseAccount])
            .rpc();

        const createdCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(createdCounter.count.toNumber(), 0);
    });
});

我们调用program.methods.initialize()并传入指令所需的账户。现在这里需要注意的是,在我们传入账户的对象中,我们使用baseAccountsystemProgram作为字段,尽管我们在rust的交易指令中把它们定义为base_accountsystem_program

这是因为anchor允许我们遵循各自语言的命名规则,对于typescript是camelCase,对于rust是snake_case

然后,传入交易的签名者数组,这是我们传入的账户以及创建该账户的用户。但你会看到我们没有在签名者数组中加入provider.wallet。这是因为signer在数组中加入了provider.wallet作为默认签名者,因此不需要明确地传递它。如果我们为一个用户单独创建一个密钥对,我们就需要在这个数组中传递它。

在RPC调用完成后,我们尝试使用创建的publicKey来获取创建的baseAccount。之后,我们断言获取的baseAccount里面的计数是0。

如果测试通过,我们就知道一切都很顺利。首先,我们需要将Solana的配置设置为使用localhost。打开终端,运行该命令:

$ solana config set --url localhost

应该显示:

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: /home/swarnab/.config/solana/id.json
Commitment: confirmed

现在,让我们测试一下代码:

$ anchor test

这应该给出一个合格的测试作为输出

这应该是一个通过测试的输出

很好! 这意味着计数器成功初始化。

测试计数器递增

让我们直接看代码:

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();

    it("initializes the counter", async () => {
        await program.methods
            .initialize()
            .accounts({
                baseAccount: baseAccount.publicKey,
                user: provider.wallet.publicKey,
                systemProgram: anchor.web3.SystemProgram.programId,
            })
            .signers([baseAccount])
            .rpc();

        const createdCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(createdCounter.count.toNumber(), 0);
    });

    // -- new changes --
    it("increments the counter", async () => {
        await program.methods
            .increment()
            .accounts({ baseAccount: baseAccount.publicKey })
            .signers([])
            .rpc();

        const incrementedCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(incrementedCounter.count.toNumber(), 1);
    });
});

在这里所做的就是对increment进行rpc调用,在调用发生后检查count是否为1。

让我们测试一下这个程序:

$ anchor test

应该显示有两个测试通过

它应该显示我们的两个测试通过了

很好! 现在知道递增逻辑也在工作。

测试计数器递减

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Counter } from "../target/types/counter";

describe("counter", () => {
    const provider = anchor.AnchorProvider.local();

    anchor.setProvider(provider);

    const program = anchor.workspace.Counter as Program<Counter>;

    let baseAccount = anchor.web3.Keypair.generate();

    it("initializes the counter", async () => {
        await program.methods
            .initialize()
            .accounts({
                baseAccount: baseAccount.publicKey,
                user: provider.wallet.publicKey,
                systemProgram: anchor.web3.SystemProgram.programId,
            })
            .signers([baseAccount])
            .rpc();

        const createdCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(createdCounter.count.toNumber(), 0);
    });

    it("increments the counter", async () => {
        await program.methods
            .increment()
            .accounts({ baseAccount: baseAccount.publicKey })
            .signers([])
            .rpc();

        const incrementedCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(incrementedCounter.count.toNumber(), 1);
    });

    // -- new changes --
    it("decrements the counter", async () => {
        await program.methods
            .decrement()
            .accounts({ baseAccount: baseAccount.publicKey })
            .signers([])
            .rpc();

        const decrementedCounter = await program.account.baseAccount.fetch(
            baseAccount.publicKey
        );

        assert.strictEqual(decrementedCounter.count.toNumber(), 0);
    });
});

这与计数器递增非常相似。最后,检查数是否为0。

$ anchor test

它应该显示我们的三个测试都通过了

它应该显示我们的三个测试都通过了

就这样了!

这使得我们在Solana上构建和测试自己的智能合约的工作结束了! 作为最后一步,让我们把计数器程序部署到Solana Devnet。

部署计数器程序

部署计数器程序,但首先我们需要改变一些东西。打开终端,运行这个命令:

$ anchor keys list

counter: 3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ

在我的例子中,3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ将是我程序的唯一ID。

前往lib.rs,修改以下一行:

declare_id!("3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ");

另一个改动是在Anchor.toml中。

[features]
seeds = false
skip-lint = false

[programs.localnet]
counter = "3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ"
[programs.devnet]
counter = "3fhorU8b8xLw75wRvAkvjRNqNgUQCZNCGJpmiRktLioQ"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "/home/swarnab/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

我们在[programs.devnet]下添加了[programs.localnet],并改变了这两个地方的计数器。

我们需要做的另一个改变是在[provider]下。将clusterlocalnet改为devnet

很好! 现在我们必须重新 build ,这是一个非常重要的步骤:

$ anchor build

现在我们需要将solana的配置改为devnet。运行命令:

$ solana config set --url devnet

应该显示:

Config File: ~/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /home/swarnab/.config/solana/id.json
Commitment: confirmed

现在我们来部署程序:

$ anchor deploy

部署成功

部署成功

如果你得到提示部署成功,这意味着你的程序部署成功。

区块链浏览器可以用程序ID查询。确保cluster被设置为devnet。程序 ID 是通过运行anchor keys list得到的。

我们的程序显示在资源管理器上。

我们的程序在区块链浏览器上显示的结果

一些额外的技术

有一些额外的工具,你可以在你的Solana dapps中使用。

Arweave

Arweave是一个社区拥有的,去中心化的永久性数据存储协议,这里是官网

Metaplex

Metaplex是一个建立在Solana区块链之上的NFT生态系统。该协议使艺术家和创作者能够像建立一个网站一样轻松地推出自我托管的NFT市场。Metaplex NFT标准是Solana生态系统中使用最多的NFT标准。请在这里查看他们。

结语

来到了本教程的结尾。希望你很喜欢它,并在本文中学到了一些东西。

我想说的一点是,Solana开发一开始可能会觉得有点难以入手,但如果你坚持下去,你会开始欣赏Solana生态系统的魅力。

让自己了解正在发生的事情,并尝试为开源的Solana项目做出贡献。

如果你在什么地方被卡住了,别忘了访问Solana Stack Exchange

祝你的Solana开发之路顺利!


本翻译由 Duet Protocol 赞助支持。

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

1 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO