简介 在这篇博客中,我们讨论一下 Solana 区块链,以及作为一个开发者如何开始在 Solana 上构建 dapp。写这篇文章时,我们考虑到了新的开发者和初学者,他们对智能合约和 dapps 仅有一点的了解。我们将探讨一些高层次的概念、工具和技术,这些都是 Solana 开发所需要的,最后我们将建立一个小的 dapp。如果这让你感到兴奋,那就加入享受吧! 开始Solana 是一个高性能的区块链,提供高吞吐量和非常低的 Gas 费用。它通过其历史证明机制实现了这一点,该机制被用来提高其 POS 共识机制的性能。 现在,谈及在 Solana 上的开发,有一定的优点和缺点。优点是,像 Solana CLI、Anchor CLI 这样的开发者工具以及它们的 SDK 都很不错,而且很容易理解和实现。但是,由于生态系统和这些工具都是非常新的,文档并不完善,缺乏必要的解释。 不过,Solana 的开发者社区非常强大,人们会热衷于帮助另一个开发者伙伴。强烈建议加入Solana[4]和Anchor[5] Discord,以了解生态系统的最新变化。此外,如果你在 Solana 开发过程中遇到任何技术问题,一个解决你问题的好地方是Solana Stack Exchange[6]。 Solana Web3 技术栈Solana 有一个非常好的工具生态系统和技术栈。让我们看看开发程序需要和使用的工具: 1. Solana 工具套件Solana Tool Suite[7]带有 Solana CLI 工具,它使开发过程变得顺利和简单。你可以用 CLI 工具执行很多任务,从部署 Solana 程序到将 SPL 代币转账到另一个账户。 在这里[8]下载工具套件。 2. RustSolana 智能合约(称为 Programs)可以用 C、C++或 Rust 编程语言编写。但最喜欢的是 Rust。 Rust[9]是一种底层的编程语言,由于其强调性能,以及类型和内存安全,已经获得了很多人的青睐。 Rust 一开始会让人觉得有点害怕,但一旦你开始掌握它,你会非常喜欢它。它有一个非常好的文档[10],它也可以作为一个很好的学习资源。其他一些关于 Rust 的资源包括Rustlings[11]和Rust-By-Example[12]。 你可以在这里[13]安装 Rust。 3. AnchorAnchor[14]是 Solana 的 Sealevel 运行时的一个框架,为编写智能合约提供了几个方便的开发者工具。Anchor 通过处理大量的模板代码使我们的开发变得更加轻松,这样我们就可以专注于重要部分。它还代表我们做了很多检查,使 Solana 程序保持安全。 Anchor Book[15]是 Anchor 当前的文档,对于使用 Anchor 编写 Solana 程序有很好的参考价值。Anchor SDK typedoc[16]有你可以在 JS 客户端使用的所有方法、接口和类。该 SDK 确实需要更好的文档。 你可以在这里[17]安装 Anchor 。 4. 前端框架为了让你的用户使用 dapp,你需要有一个能够与区块链通信的前端。你可以用任何一个常见的框架(React / Vue / Angular)编写你的客户端逻 辑。 如果你想用这些框架构建你的客户端,你需要在你的系统中安装 NodeJS。你可以在这里[18]安装它。 建立一个 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)的东西。基本上有三种类型的账户: - 程序账户 :存储可执行代码的账户,是合约被部署的地方。
- 存储账户:存储数据的账户,通常情况下,它们存储一个程序的状态。
- 代币账户: 储存不同 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的结构体,在这里我们声明了这个交易需要的所有账户,让我们一个一个的看。 - base_account: 要初始化 base_account,我们的指令中需要有这个账户(显然)。在account属性中,我们传入 3 个参数。init声明我们正在初始化账户。现在可能会想到的一件事是,如果它还没有被初始化,我们如何在指令中传递baseAccount。我们可以这样做的原因是,之后在写测试时也会看到,我们将为baseAccount的创建并传递一个密钥对。只有在指令成功发生后,baseAccount账户才会在 Solana 链上为我们创建的密钥对创建。 payer声明了将付费创建账户的用户。这里需要注意的是,在链上存储数据不是免费的。它需要花费 SOL。在这种情况下,user账户将支付租金来初始化base_account。space表示我们需要给账户的空间数量。8 个字节用于一个独一无二的discriminator,16 个字节用于计数数据。
- 用户:user是授权者,其有权签署初始化base_account的交易。
- system_program: system_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()并传入指令所需的账户。现在这里需要注意的是,在我们传入账户的对象中,我们使用baseAccount和systemProgram作为字段,尽管我们在 rust 的交易指令中把它们定义为base_account和system_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] 下。将cluster从localnet改为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
部署成功 如果你得到提示部署成功,这意味着你的程序部署成功。 去区块链浏览器[19]可以用程序 ID 查询。确保cluster被设置为devnet。程序 ID 是通过运行anchor keys list得到的。 我们的程序在区块链浏览器上显示的结果 一些额外的技术有一些额外的工具,你可以在你的 Solana dapps 中使用。 ArweaveArweave 是一个社区拥有的,去中心化的永久性数据存储协议,这里是官网[20]。 MetaplexMetaplex 是一个建立在 Solana 区块链之上的 NFT 生态系统。该协议使艺术家和创作者能够像建立一个网站一样轻松地推出自我托管的 NFT 市场。Metaplex NFT 标准是 Solana 生态系统中使用最多的 NFT 标准。请在这里[21]查看他们。 结语来到了本教程的结尾。希望你很喜欢它,并在本文中学到了一些东西。 我想说的一点是,Solana 开发一开始可能会觉得有点难以入手,但如果你坚持下去,你会开始欣赏 Solana 生态系统的魅力。 让自己了解正在发生的事情,并尝试为开源的 Solana 项目做出贡献。 如果你在什么地方被卡住了,别忘了访问Solana Stack Exchange[22]。 祝你的 Solana 开发之路顺利!
|