Solana Seahorse开发入门

Seahorse是一个使用Python开发Solana程序的框架,本指南非常适合旨在学习 Solana 开发的Python开发人员。

Solana Seahorse开发入门
一键发币: SOL | BNB | ETH | BASE | Blast | ARB | OP | POLYGON | AVAX | FTM | OK

在 Solana 上进行开发是出了名的困难,学习 Rust 是一个重大障碍。 Anchor框架抽象化了 Solana 构建过程中涉及的许多复杂性,但是,学习 Rust 仍然是寻求探索 Solana 生态系统的新开发人员的障碍。 因此就有了seahorse lang,这是一个使用Anchor 构建的Python 开发Solana 程序的框架。

这是使用 Seahorse lang 构建 Solana 应用程序的简单指南。 本指南非常适合旨在学习 Solana 开发的新开发人员。

1、概述和准备

每一项新技术都应该用来解决人类最棘手的问题。 因此,我们将使用 Solana 区块链构建一个程序,通过让用户投票选出他们最喜欢的口味(松脆的还是光滑的)来找出最好的花生酱口味。

首先让我们安装构建所需的命令行工具。 我们需要 SolanaAnchorNodeJSSeahorse。 提供的链接包含安装这些工具的分步指南以及它们所需的依赖项(例如 Rust)。

完成后,你应该能够成功运行以下命令:

solana -V
anchor -V
node -v
seahorse -V

我使用的版本是:

  • solana 1.9.2
  • anchor 0.25.0
  • node 16.8.0
  • seahorse 0.1.6

2、seahorse入门

现在我们可以使用 seahorse init 来初始化项目:

seahorse init crunchy_vs_smooth

这将创建多个文件,如果你使用过anchor,你可能会熟悉项目的结构,seahorse只是添加了一个新文件夹来编写python程序,我们将在这里编写我们的seahorse程序:

crunchy_vs_smooth/programs_py/crunchy_vs_smooth.py

我们将在这里用typescript编写测试:

crunchy_vs_smooth/tests/crunchy_vs_smooth.ts

3、Solana账户模型简介

在开始之前,我想先谈谈 Solana 在链上存储和处理数据的独特方式,这将进一步帮助我们了解 Solana 程序的结构,如果你已经了解这一点,可以安全地跳过这一部分。

Solana 区块链上的所有内容都存储在一个帐户中。 Solana 上有 3 种类型的账户:

  • 数据帐户(data account)用于存储数据。
  • 程序帐户(program account)用于存储可执行程序。
  • 原生帐户(native account)表示 Solana 上的本机程序,例如系统、权益和投票。

每个帐户都映射到一个地址(公钥)和一个所有者(程序帐户的地址)。

在 Solana 程序中,状态和代码分开存在,程序不存储任何数据,而执行时所有数据应作为参数传递给它们。

因此,为了简单起见,Solana 上的所有内容都存储在一个帐户中,程序存储在一种特殊类型的帐户中,该帐户被标记为可执行且不存储任何数据,为了存储数据,我们需要创建另一个由程序拥有的帐户 可以修改数据。

可以在此处阅读有关 Solana 帐户模型的更多信息。

4、PDA 和强制唯一性

我想在这里切题解释 PDA(程序派生地址)的概念及其在 Solana 程序中的用例之一。 如果你已经了解,可以跳过此部分。

举个例子,每当你登录FaceBook时,后端FB会根据你的凭据决定你的访问级别,基本上,你可以编辑和查看自己帐户的数据,但只能查看不能修改其他用户的帐户。

Facebook 强制唯一性

从 Facebook 的角度来看,他们可以通过将你的凭据映射到只能由你修改的唯一帐户空间,从逻辑上分离存储在其数据库中的数据。

与在链上存储数据的方式相同,我们根据一些用户输入(通常是所有者的公钥)、种子和拥有程序的地址生成一个唯一的公钥,而无需相应的私钥。 生成的唯一地址称为 PDA 或程序派生地址。

本质上,我们正在构建下面提到的类似于哈希图的结构来在链上存储数据。

Solana 上的 PDA

由于这些地址没有私钥,任何人都无法签署和修改相应帐户中存储的数据。 但是,拥有该帐户的程序可以在收到有效种子生成的正确碰撞时修改数据。

PDA是使用Anchor提供的 findProgramAddress方法生成的,它以seeds(Uint8Array | Buffer)和programId(PublicKey)作为输入。 该函数迭代各个地址,直到找到一个与种子组合时生成不在 ed25519 曲线上的有效公钥(没有私钥)的地址。 最后,它返回一个 PDA(PublicKey) 和一个bump(number)。

稍后我们编写测试时会研究生成 PDA 的过程。 除了帮助在链上存储数据之外,PDA 还允许程序签署指令。 可以在此处阅读有关 PDA 的更多信息。

5、编写我们的 Seahorse 程序

我们终于准备好编写我们的程序了。 首先,让我们通过最终的程序来直观地了解典型 Solana 程序的结构。

# crunchy_vs_smooth
# Built with Seahorse v0.1.6


from seahorse.prelude import *

# This will be updated once the project is build, your program id can be found at crunchy_vs_smooth/target/idl/[your_project_name].json
declare_id("Fo9d34XdUczXdNt9jkqrQseyUQys9bmTDC31utY3zt5x")


# Here we define all our instructions, each of the method below as an RPC end point which can be invoked by clients.
@instruction
def init(owner: Signer, voter: Empty[VoteAccount], vote_account_bump: u8):
    # As a new user connects, we create a new voter PDA account for him and intialize the account.
    init_voter = voter.init(payer=owner, seeds=["Voter", owner])
    # Assign the owner or the Signer of the one initialize the accouunt to the user's newly created VoteAccount owner.
    init_voter.owner = owner.key()
    # Assign the bump to the one initializing the accouunt to the user's newly created VoteAccount bump.
    init_voter.bump = vote_account_bump


# To vote crunchy
@instruction
def vote_crunchy(owner: Signer, vote: VoteAccount):
    # Check if the public key of the signer is the same as the owner in the vote account.
    assert owner.key() == vote.owner, "This is not your Vote account!"
    # Increment the crunchy variable in the user's VoteAccount
    vote.crunchy += 1


# To vote smooth
@instruction
def vote_smooth(owner: Signer, vote: VoteAccount):
    # Check if the public key of the signer is the same as the owner in the vote account.
    assert owner.key() == vote.owner, "This is not your Vote account!"
    # Increment the smooth variable in the user's VoteAccount
    vote.smooth += 1


# Defining the account which will be stored on-chain for every unique wallet interacting with our program.
class VoteAccount(Account):
    owner: Pubkey
    crunchy: u64
    smooth: u64
    bump: u8

在Anchor Rust 程序中,你经常会发现两件事以逻辑形式分隔代码:

  • 修饰符上的 #[program] 宏包含所有方法,也称为指令,它们包含所有逻辑并充当客户端调用的 RPC 端点。
  • 结构体上的 #[account] 宏,它在链上创建新帐户以供我们存储数据。

对于每个 Anchor 程序,此结构或多或少保持相同。

在我们的 Seahorse 程序中,你会发现指令方法和继承自父帐户的帐户类之间具有相同的逻辑分离。

对于我们来说, initvote_crunchyvote_smooth 是三个指令,它们充当客户端调用的 RPC 端点,我们在编写测试时将使用它们。

最后,我们定义一个帐户来存储数据 VoteAccount,为每个连接与我们的 dapp 交互的用户创建一个新的 VoteAccount

当新用户连接时,我们将通过调用 init 指令来初始化其链上帐户。

# Here we define all our instructions, each of the method below as an RPC end point which can be invoked by clients.
@instruction
def init(owner: Signer, voter: Empty[VoteAccount], vote_account_bump: u8):
    # As a new user connects, we create a new voter PDA account for him and intialize the account.
    init_voter = voter.init(payer=owner, seeds=["Voter", owner])
    # Assign the owner or the Signer of the one initialize the accouunt to the user's newly created VoteAccount owner.
    init_voter.owner = owner.key()
    # Assign the bump to the one initializing the accouunt to the user's newly created VoteAccount bump.
    init_voter.bump = vote_account_bump

init 指令接收为帐户付款的所有者、投票者、PDA 和相应的碰撞

然后,第一行创建一个新的 VoteAccount 帐户,验证作为投票者传入的 PDA 是否是从正确的种子生成的,在我们的例子中是字符串“Voter”和所有者帐户。 它还将付款人指定为所有者。

然后它创建一个空的 VoteAccount 分配所有者并传递并脆脆地平滑地初始化为 0。 VoteAccount 看起来像这样:

# Defining the account which will be stored on-chain for every unique wallet interacting with our program.
class VoteAccount(Account):
    owner: Pubkey
    crunchy: u64
    smooth: u64
    bump: u8

该帐户将用于在链上存储唯一的用户凭证及其投票状态。

最后,我们创建单独的说明来投票选出你最喜欢的花生酱口味。

# To vote crunchy
@instruction
def vote_crunchy(owner: Signer, vote: VoteAccount):
    # Check if the public key of the signer is the same as the owner in the vote account.
    assert owner.key() == vote.owner, "This is not your Vote account!"
    # Increment the crunchy variable in the user's VoteAccount
    vote.crunchy += 1


# To vote smooth
@instruction
def vote_smooth(owner: Signer, vote: VoteAccount):
    # Check if the public key of the signer is the same as the owner in the vote account.
    assert owner.key() == vote.owner, "This is not your Vote account!"
    # Increment the smooth variable in the user's VoteAccount
    vote.smooth += 1

上述两条指令以所有者和投票PDA作为输入,并验证调用指令的所有者是否与PDA帐户中的所有者匹配。 因此,在调用任何投票指令之前调用 init 指令非常重要。 如果 VoteAccount 未初始化,则指令将失败,因为 VoteAccount.owner 将为空。

最后,我们可以从项目的根目录运行 seahorse build。 如果命令成功,那么我们已经成功构建了第一个 Seahorse 项目。

6、部署链上程序

一旦程序构建成功。 它将填充 programs/crunchy_vs_smooth/src/lib.rs  Anchor等效的Rust 程序。

接下来通过运行检查 solana 配置是否设置为 localhost

solana config get

如果它不在本地主机上,则按如下方式设置:

solana config set — url localhost

完成后,打开一个新终端并运行 solana-test-validator,这应该会在本地主机上启动 solana 验证器。 给自己空投一些代币以在本地部署程序: solana airdrop 1000

现在我们已经准备好部署程序了,运行 anchor deploy

如果部署成功,部署成功之前的倒数第二行将包含你唯一的programId。

我们需要修改 rust 和 python 程序中的anchor.toml 和declare_id 宏中提到的programId 。

我们现在准备编写测试。

7、编写测试

我们已经创建了三个指令,因此我们将编写三个测试。

在此之前,我们需要初始化帐户。

describe("crunchy_vs_smooth", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

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

  const [voter, bump] = web3.PublicKey.findProgramAddressSync(
    [Buffer.from("Voter"), provider.wallet.publicKey.toBuffer()],
    program.programId
  );

Anchor 使用 `anchor.AnchorProvider.env() `方便地为我们模拟用户钱包,我们将使用它作为我们的提供商钱包进行测试。

接下来,我们使用 anchor.workspace导入我们的程序,这也相对简单,这要归功于anchor。

下一步很重要,在这里我们找到了一个 PDA,它使用与我们的程序中` init` 指令中提到的相同的种子:

init_voter = voter.init(payer=owner, seeds=[“Voter”, owner])

我们使用所有者的“Voter”字符串作为提供者钱包的公钥,以及种子数组,我们还传递了programId。 这些输入的组合将给出投票者(PDA)和用于查找该地址的凹凸。

现在我们继续进行实际测试。 首先,我们测试一下 init指令:

  it("Initialized the voter!", async () => {
    await program.methods
      .init(bump)
      .accounts({
        owner: provider.wallet.publicKey,
        voter: voter,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();

    let currentVoteAccountState = await program.account.voteAccount.fetch(
      voter
    );
    assert.equal(0, currentVoteAccountState.crunchy.toNumber());
    assert.equal(0, currentVoteAccountState.smooth.toNumber());
  });

在这里,我们传递了指令所需的bump和帐户。

def init(owner: Signer, voter: Empty[VoteAccount], vote_account_bump: u8):
  • bump 是我们之前创建 PDA 时生成的凹凸。
  • owner 是提供者钱包的公钥。
  • voter 就是我们之前生成的 PDA。

我们还需要传入 system program

在运行 .rpc() 时,它将在本地网络上为我们创建一个事务,调用我们部署的程序上的 init 指令。

完成后,我们将通过将 PDA 地址投票者传递给 voteAccount 来检查程序的状态

最后,我们将断言 voteAccount 中的 crunchy 和 smooth 都为 0。 确认我们的帐户已成功创建。

接下来,我们将测试 vote_crunchyvote_smooth 指令。

  it("Vote Smooth", async () => {
    const voteSmooth = await program.methods
      .voteSmooth()
      .accounts({ owner: provider.wallet.publicKey, vote: voter })
      .rpc();

    let currentVoteAccountState = await program.account.voteAccount.fetch(
      voter
    );

    assert.equal(1, currentVoteAccountState.smooth.toNumber());
    assert.equal(0, currentVoteAccountState.crunchy.toNumber());
  });

  it("Vote Crunchy", async () => {
    const voteCrunchy = await program.methods
      .voteCrunchy()
      .accounts({ owner: provider.wallet.publicKey, vote: voter })
      .rpc();

    let currentVoteAccountState = await program.account.voteAccount.fetch(
      voter
    );

    assert.equal(1, currentVoteAccountState.crunchy.toNumber());
    assert.equal(1, currentVoteAccountState.smooth.toNumber());
  });

这两个指令都期望所有者公钥和投票 PDA 帐户作为输入。

def vote_smooth(owner: Signer, vote: VoteAccount):

def vote_crunchy(owner: Signer, vote: VoteAccount):

我们传递之前初始化的所有者和投票者帐户,然后像以前一样获取帐户的状态。

在第一条指令后 vote_smooth smooth 应增加到 1,而 crunchy 应保持为零,在第二条指令 vote_crunchy 后 crunchy 和 smooth 均应为 1。

最终测试文件应如下所示:

import * as anchor from "@project-serum/anchor";
import { web3, Program } from "@project-serum/anchor";
import { assert } from "chai";
import { CrunchyVsSmooth } from "../target/types/crunchy_vs_smooth";

describe("crunchy_vs_smooth", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

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

  const [voter, bump] = web3.PublicKey.findProgramAddressSync(
    [Buffer.from("Voter"), provider.wallet.publicKey.toBuffer()],
    program.programId
  );

  console.log(bump);

  it("Initialized the voter!", async () => {
    await program.methods
      .init(bump)
      .accounts({
        owner: provider.wallet.publicKey,
        voter: voter,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();

    let currentVoteAccountState = await program.account.voteAccount.fetch(
      voter
    );
    assert.equal(0, currentVoteAccountState.crunchy.toNumber());
    assert.equal(0, currentVoteAccountState.smooth.toNumber());
  });

  it("Vote Smooth", async () => {
    const voteSmooth = await program.methods
      .voteSmooth()
      .accounts({ owner: provider.wallet.publicKey, vote: voter })
      .rpc();

    let currentVoteAccountState = await program.account.voteAccount.fetch(
      voter
    );

    assert.equal(1, currentVoteAccountState.smooth.toNumber());
    assert.equal(0, currentVoteAccountState.crunchy.toNumber());
  });

  it("Vote Crunchy", async () => {
    const voteCrunchy = await program.methods
      .voteCrunchy()
      .accounts({ owner: provider.wallet.publicKey, vote: voter })
      .rpc();

    let currentVoteAccountState = await program.account.voteAccount.fetch(
      voter
    );

    assert.equal(1, currentVoteAccountState.crunchy.toNumber());
    assert.equal(1, currentVoteAccountState.smooth.toNumber());
  });
});

完成后,从根运行 anchor test,所有 3 个测试都应该通过。

运行测试时你可能会遇到一些错误,这是由于 rust 程序中的某个结构体上方有 #[derive(Debug)] 造成的,请删除它并再次运行 anchor test。 其他警告可以忽略。

8、结束语

Seahorse 极大地简化了在 Solana 上开发程序的过程,同时它还充当 Python 程序员熟悉 Rust 程序的门户,因为 Seahorse 会自动为你生成 Anchor Rust 程序。

本教程的原始代码可以在这里找到。


原文链接:An introduction to writing applications on Solana with Anchor and Seahorse lang

DefiPlot翻译整理,转载请标明出处

通过 NowPayments 打赏