分类 Solana 下的文章

Solana基础 - Solana中的存储成本、最大存储容量和账户调整


文章详细介绍了Solana区块链中账户存储空间租金的计算方式及其相关概念,包括租金豁免、存储成本比较、账户大小限制和调整、以及部署程序的成本预估。

在分配存储空间时,付款方必须支付每字节分配一定数量的SOL。

Solana称之为“租赁”。这个名称有点误导,因为它暗示需要每月充值,但情况并非总是如此。一旦租金支付,就不再需要支付,即使两年过去了。当支付了两年的租金后,账户被认为是“免租”的。

这个名称来源于Solana最初按字节每年的形式向账户收费。如果你只支付了半年的租金,那么你的账户将在六个月后被删除。如果你提前支付了两年的租金,账户就会“免租”。该账户将不再需要支付租金。如今,所有账户都必须是免租的;你不能支付少于两年的租金。

虽然租金是按照“每字节”计算的,但零数据的账户不是免费的;Solana仍需要对其进行索引并存储与之相关的元数据。

当账户初始化时,所需租金的数量会在后台计算;你无需明确计算租金。

然而,你确实想要能够预测存储的成本,以便能够恰当地设计你的应用程序。

如果你想要一个快速的估算,运行 solana rent <字节数> 在命令行中会给出快速的答案:solana rent 32

如前所述,分配零字节是非免费的:solana rent 0

让我们看看这个费用是如何计算的。

Anchor Rent Module给我们提供了一些与租金相关的常量:

ACCOUNT_STORAGE_OVERHEAD:该常量的值为128(字节),顾名思义,一个空账户有128字节的开销。
DEFAULT_EXEMPTION_THRESHOLD:该常量的值为2.0(浮点数64),指的是提前支付两年的租金使得账户免于支付进一步的租金。
DEFAULT_LAMPORTS_PER_BYTE_YEAR:该常量的值为3,480,意味着每字节每年需要3,480 lamports。由于我们需要支付两年, 每字节将花费6,960 lamports。
以下Rust程序打印出一个空账户将花费的金额。请注意,结果与上面的 solana rent 0 截图相符:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::rent as rent_module;

declare_id!("BfMny1VwizQh89rZtikEVSXbNCVYRmi6ah8kzvze5j1S");

[program]

pub mod rent {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let cost_of_empty_acc = rent_module:: ACCOUNT_STORAGE_OVERHEAD as f64 * 
                            rent_module::DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 *
                            rent_module::DEFAULT_EXEMPTION_THRESHOLD; 

    msg!("创建空账户的成本: {}", cost_of_empty_acc);
    // 890880

    Ok(())
}

}

[derive(Accounts)]

pub struct Initialize {}
如果我们想计算一个非空账户的费用,那么我们只需将字节数加入到空账户的费用中,如下所示:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::rent as rent_module;

declare_id!("BfMny1VwizQh89rZtikEVSXbNCVYRmi6ah8kzvze5j1S");

[program]

pub mod rent {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let cost_of_empty_acc = rent_module:: ACCOUNT_STORAGE_OVERHEAD as f64 * 
                            rent_module::DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 *
                            rent_module::DEFAULT_EXEMPTION_THRESHOLD;

    msg!("创建空账户的成本: {}", cost_of_empty_acc);
    // 890,880 lamports

    let cost_for_32_bytes = cost_of_empty_acc + 
                            32 as f64 * 
                            rent_module::DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 *
                            rent_module::DEFAULT_EXEMPTION_THRESHOLD;

    msg!("创建32字节账户的成本: {}", cost_for_32_bytes);
    // 1,113,600 lamports
    Ok(())
}

}

[derive(Accounts)]

pub struct Initialize {}
同样,请注意该程序的输出与命令行上的输出相符。

比较存储成本与ETH
截至本文写成时,ETH的价值约为 $2,425。初始化一个新账户的费用为22,100 gas,因此我们可以计算32字节的gas成本为$0.80,假设gas费为15 gwei。

目前,Solana的价格为
90
/
S
O
L
,因此支付
1
,
113
,
600
l
a
m
p
o
r
t
s
以初始化
32
字节存储将花费
90/SOL,因此支付1,113,600lamports以初始化32字节存储将花费0.10。

然而,ETH的市场资本是SOL的7.5倍,因此如果SOL的市场资本与ETH相同,SOL的当前价格将为$675,而32字节的存储将花费 $0.75。

Solana有一个永久的通胀模型,最终将收敛到每年1.5%,这应有助于反映出存储随着时间的推移变得更便宜,根据摩尔定律,成本相同的晶体管密度每18个月翻倍。

请记住,从字节到加密的转换是协议中设置的常量,可能在任何时候由于硬分叉而改变。

余额低于2年免租阈值的账户会减少直到账户被删除
可以在这里阅读一个用户钱包账户逐渐被“耗尽”的趣味Reddit帖子:https://www.reddit.com/r/solana/comments/qwin1h/my_sol_balance_in_the_wallet_is_decreasing/

原因是该钱包的余额低于租赁豁免阈值,Solana运行时正在慢慢减少账户余额以支付租金。

如果由于余额低于免租阈值而导致钱包最终被删除,可以通过向其发送更多SOL进行“复活”,但如果数据存储在账户中,则该数据将会丢失。

大小限制
当我们初始化一个账户时,大小不得超过10,240字节。

练习:创建一个基本的存储初始化程序并设置space=10241。这比限制高出1字节。你应该会看到类似以下的错误:solana 账户无法初始化,因为超过了大小限制

更改账户大小
如果你需要增加账户的大小,可以使用realloc宏。这在账户存储一个向量并需要更多空间时可能会很方便。增加1000字节的代码在increase_account_size函数和IncreaseAccountSize上下文结构中可以找到(请查看以下代码中的大写注释):

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

[program]

pub mod basic_storage {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
}

pub fn increase_account_size(ctx: Context<IncreaseAccountSize>) -> Result<()> {
    Ok(())
}

}

[derive(Accounts)]

pub struct IncreaseAccountSize<'info> {

#[account(mut,
          // ***** 增加1,000 BYTE点在这里 *****
          realloc = size_of::<MyStorage>() + 8 + 1000,
          realloc::payer = signer,
          realloc::zero = false,
          seeds = [],
          bump)]
pub my_storage: Account<'info, MyStorage>,

#[account(mut)]
pub signer: Signer<'info>,

pub system_program: Program<'info, System>,

}

[derive(Accounts)]

pub struct Initialize<'info> {

#[account(init,
          payer = signer,
          space=size_of::<MyStorage>() + 8,
          seeds = [],
          bump)]
pub my_storage: Account<'info, MyStorage>,

#[account(mut)]
pub signer: Signer<'info>,

pub system_program: Program<'info, System>,

}

[account]

pub struct MyStorage {
x: u64,
}
在增加账户大小时,如果你不想删除账户数据,请确保设置realloc::zero = false(在上面的代码中)。如果你希望账户数据被设置为全零,请使用realloc::zero = true。你不需要更改测试。该宏会为你在后台处理这些。

练习:在测试中初始化一个账户,然后调用increase_account_size函数。通过在命令行中执行solana account 查看账户大小。你需要在本地验证器上执行此操作,以使账户持续存在。

最大Solana账户大小
每次重新分配的最大账户增加为10240。Solana中账户的最大大小为10MB。

预测程序部署的成本
部署Solana程序的主要费用来自支付存储字节码的租金。字节码存储在与anchor deploy返回的地址不同的单独账户中。

下面的屏幕截图展示了如何获取此信息:程序部署成本

目前,简单的Hello World程序的部署费用略高于2.47 SOL。通过直接编写原生Rust代码而不是使用Anchor框架,费用可以显著降低,但我们不建议在你完全理解Anchor默认消除的所有安全风险之前这样做。

转载:https://learnblockchain.cn/article/11420


Solana基础 - 在Solana中创建“映射”和“嵌套映射”


本文详细介绍了如何在 Solana 中使用 seeds 参数来实现类似于 Solidity 中的映射和嵌套映射,并提供了 Rust 和 Typescript 的代码示例。
在之前的教程中,seeds=[] 参数总是为空。如果我们向其中放入数据,它的表现就像 Solidity 映射中的一个或多个键。

考虑以下示例:

contract ExampleMapping {

    struct SomeNum {
        uint64 num;
    }

    mapping(uint64 => SomeNum) public exampleMap;

    function setExampleMap(uint64 key, uint64 val) public {
        exampleMap[key] = SomeNum(val);
    }
}

我们现在创建一个 Solana Anchor 程序 example_map

初始化映射:Rust

起初,我们只会展示初始化步骤,因为它将引入一些需要解释的新语法。

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("DntexDPByFxpVeBSjd6nLqQQSqZmSaDkP8TUbcJ9jAgt");

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

    pub fn initialize(ctx: Context<Initialize>, key: u64) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(key: u64)]
pub struct Initialize<'info> {

    #[account(init,
              payer = signer,
              space = size_of::<Val>() + 8,
              seeds=[&key.to_le_bytes().as_ref()],
              bump)]
    val: Account<'info, Val>,

    #[account(mut)]
    signer: Signer<'info>,

    system_program: Program<'info, System>,
}

#[account]
pub struct Val {
    value: u64,
}

以下是你可以将映射视为的方式:

&key.to_le_bytes().as_ref() 中的 seeds 参数 key 可以看作是一个类似于 Solidity 构造形式的“键”:

mapping(uint256 => uint256) myMap;
myMap[key] = val

代码中不熟悉的部分是 #[instruction(key: u64)]seeds=[&key.to_le_bytes().as_ref()]

seeds = [&key.to_le_bytes().as_ref()]

seeds 中的项预期是字节。然而,我们传入的是一个 u64,它不是字节类型。为了将其转换为字节,我们使用 to_le_bytes()。“le” 是指 “小端”。seeds 不必编码为小端字节,我们只是在这个例子中选择了它。只要你保持一致,大端也可以使用。如果要转换为大端,我们将使用 to_be_bytes()

[instruction(key: u64)]

为了在 initialize(ctx: Context<Initialize>, key: u64) 中“传递”参数 key,我们需要使用 instruction 宏,否则我们的 init 宏没有办法“看到”来自 initializekey 参数。

初始化映射:Typescript

以下代码展示了如何初始化账户:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ExampleMap } from "../target/types/example_map";

describe("example_map", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Initialize mapping storage", async () => {
    const key = new anchor.BN(42);
    const seeds = [key.toArrayLike(Buffer, "le", 8)];

    let valueAccount = anchor.web3.PublicKey.findProgramAddressSync(
      seeds,
      program.programId
    )[0];

    await program.methods.initialize(key).accounts({val: valueAccount}).rpc();
  });
});

代码 key.toArrayLike(Buffer, "le", 8) 指定我们试图使用来自 key 的值创建一个大小为 8 字节的字节缓冲区。我们选择 8 字节,因为我们的键是 64 位,64 位是 8 字节。“le” 是小端,以便与 Rust 代码匹配。

映射中的每个“值”都是一个单独的账户,必须单独初始化。

设置映射:Rust

我们需要的额外 Rust 代码来设置值。这里的语法应该是熟悉的。

// 在 #[program] 模块内
pub fn set(ctx: Context<Set>, key: u64, val: u64) -> Result<()> {
    ctx.accounts.val.value = val;
    Ok(())
}

//...

#[derive(Accounts)]
#[instruction(key: u64)]
pub struct Set<'info> {
    #[account(mut)]
    val: Account<'info, Val>,
}

设置和读取映射:Typescript

因为我们在客户端(Typescript)推导出存储值的账户地址,所以我们就像处理 seeds 数组为空的账户那样读取和写入它。读取和写入 Solana 账户数据 的语法与之前的教程是相同的:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ExampleMap } from "../target/types/example_map";

describe("example_map", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Initialize and set value", async () => {
    const key = new anchor.BN(42);
    const value = new anchor.BN(1337);

    const seeds = [key.toArrayLike(Buffer, "le", 8)];
    let valueAccount = anchor.web3.PublicKey.findProgramAddressSync(
      seeds,
      program.programId
    )[0];

    await program.methods.initialize(key).accounts({val: valueAccount}).rpc();

    // 设置账户
    await program.methods.set(key, value).accounts({val: valueAccount}).rpc();

    // 读取账户
    let result = await program.account.val.fetch(valueAccount);

    console.log(`值 ${result.value} 被存储在 ${valueAccount.toBase58()}`);
  });
});

澄清“嵌套映射”

在像 Python 或 JavaScript 这样的语言中,真正的嵌套映射是一个指向另一个哈希图的哈希图。

然而,在 Solidity 中,“嵌套映射”只是一个拥有多个键的单一映射,表现得就像它们是一个键。

在“真正的”嵌套映射中,你可以只提供第一个键并返回另一个哈希图。

Solidity 的“嵌套映射”不是“真正的”嵌套映射:你不能提供一个键并获得一个映射返回:你必须提供所有的键才能得到最终结果。

如果你使用 seeds 来模拟类似于 Solidity 的嵌套映射,你将面临相同的限制。你必须提供所有的 seeds——Solana 不会接受只有一个 seed。

初始化嵌套映射:Rust

seeds 数组可以容纳任意数量的项,这类似于 Solidity 中的嵌套映射。当然,它受到每笔交易施加的计算限制。下面显示了初始化和设置的代码。

我们不需要任何特殊的语法,只是需要更多的函数参数并在 seeds 中放入更多的项,因此我们将展示完整的代码,而不再进一步解释。

Rust 嵌套映射

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("DntexDPByFxpVeBSjd6nLqQQSqZmSaDkP8TUbcJ9jAgt");

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

    pub fn initialize(ctx: Context<Initialize>, key1: u64, key2: u64) -> Result<()> {
        Ok(())
    }

    pub fn set(ctx: Context<Set>, key1: u64, key2: u64, val: u64) -> Result<()> {
        ctx.accounts.val.value = val;
        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(key1: u64, key2: u64)] // 添加的新键参数
pub struct Initialize<'info> {

    #[account(init,
              payer = signer,
              space = size_of::<Val>() + 8,
              seeds=[&key1.to_le_bytes().as_ref(), &key2.to_le_bytes().as_ref()], // 2 个 seeds
              bump)]
    val: Account<'info, Val>,

    #[account(mut)]
    signer: Signer<'info>,

    system_program: Program<'info, System>,
}

#[derive(Accounts)]
#[instruction(key1: u64, key2: u64)] // 添加的新键参数
pub struct Set<'info> {
    #[account(mut)]
    val: Account<'info, Val>,
}

#[account]
pub struct Val {
    value: u64,
}

Typescript 嵌套映射

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ExampleMap } from "../target/types/example_map";

describe("example_map", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Initialize and set value", async () => {
    // 我们现在有两个键
    const key1 = new anchor.BN(42);
    const key2 = new anchor.BN(43);
    const value = new anchor.BN(1337);

    // seeds 现在有两个值
    const seeds = [key1.toArrayLike(Buffer, "le", 8), key2.toArrayLike(Buffer, "le", 8)];
    let valueAccount = anchor.web3.PublicKey.findProgramAddressSync(
      seeds,
      program.programId
    )[0];

    // 函数现在使用两个键
    await program.methods.initialize(key1, key2).accounts({val: valueAccount}).rpc();
    await program.methods.set(key1, key2, value).accounts({val: valueAccount}).rpc();

    // 读取账户
    let result = await program.account.val.fetch(valueAccount);
    console.log(`值 ${result.value} 被存储在 ${valueAccount.toBase58()}`);
  });
});

练习 : 修改上述代码以形成一个使用三个键的嵌套映射。

初始化多个映射

实现多个映射的一种简单方法是向 seeds 数组中添加另一个变量,并将其视为“索引”第一个映射、第二个映射等等。

以下代码展示了初始化 which_map 的示例,该映射仅持有一个键。

#[derive(Accounts)]
#[instruction(which_map: u64, key: u64)]
pub struct InitializeMap<'info> {

    #[account(init,
              payer = signer,
              space = size_of::<Val1>() + 8,
              seeds=[&which_map.to_le_bytes().as_ref(), &key.to_le_bytes().as_ref()],
              bump)]
    val: Account<'info, Val1>,

    #[account(mut)]
    signer: Signer<'info>,

    system_program: Program<'info, System>,
}

练习 : 完成 Rust 和 Typescript 代码以创建一个具有两个映射的程序:第一个映射使用一个键,第二个映射使用两个键。考虑如何在指定第一个映射时,将两个级别的映射转换为单级映射。

https://learnblockchain.cn/article/11407


Solana基础 - 使用 Solana web3 js 和 Anchor 读取账户数据


本教程详细介绍了如何通过Solana的web3 Javascript客户端直接读取账户数据,并展示了如何在Web应用的前端实现这一功能。教程中首先使用Rust代码初始化并写入数据,然后通过TypeScript读取和反序列化数据,并进一步演示了如何读取由其他Anchor程序创建的账户数据。
本教程展示了如何直接从 Solana web3 Javascript 客户端读取账户数据,以便 web 应用能够在前端读取它。

在上一个教程中,我们使用 solana account <account address> 来读取我们写入的数据,但如果我们正在网站上构建一个 dApp,这种方法是行不通的。

相反,我们必须计算存储账户的地址,读取数据,并从 Solana web3 客户端反序列化数据。

想象一下,如果在 Ethereum 中我们希望避免使用公共变量或视图函数,但仍然想在前端显示它们的值。要在不公开它们或添加视图函数的情况下查看存储变量中的值,我们将使用 getStorageAt(contract_address, slot) API。在 Solana 中,我们将做类似的事情,不过我们只需传入程序的地址,并派生出其存储账户的地址。

以下是上一个教程中的 Rust 代码。它初始化 MyStorage 并使用 set 函数写入 x。在本教程中我们不修改它:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
        ctx.accounts.my_storage.x = new_x;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut, seeds = [], bump)]
    pub my_storage: Account<'info, MyStorage>,
}

#[derive(Accounts)]
pub struct Initialize<'info> {

    #[account(init,
              payer = signer,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub signer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[account]
pub struct MyStorage {
    x: u64,
}

以下是 Typescript 单元测试,它:

  1. 初始化账户
  2. 170 写入存储
  3. 使用 fetch 函数读取值:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";

describe("basic_storage", () => { 
    anchor.setProvider(anchor.AnchorProvider.env());

    const program = anchor.workspace.BasicStorage as Program;

    it("已初始化!", async () => { 
        const seeds = [] 
        const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

        console.log("存储账户地址是", myStorage.toBase58());

        await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
        await program.methods.set(new anchor.BN(170)).accounts({myStorage: myStorage}).rpc();

        // ***********************************
        // *** 新代码以读取结构 ***
        // ***********************************
        let myStorageStruct = await program.account.myStorage.fetch(myStorage);
        console.log("x 的值是:", myStorageStruct.x.toString());

    }); 
});

在 Anchor 中查看账户可以使用:

```typescript
let myStorageStruct = await program.account.myStorage.fetch(myStorage);
console.log("x 的值是:", myStorageStruct.x.toString());

Anchor 会自动计算 MyStorage 账户的地址,读取它并将其格式化为 Typescript 对象。

要理解 Anchor 如何神奇地将 Rust 结构转换为 Typescript 结构,我们来看看 target/idl/basic_storage.json 中的 IDL。在 JSON 的底部,我们可以看到程序正在创建的结构定义:

"accounts": [
  {
    "name": "MyStorage",
    "type": {
      "kind": "struct",
      "fields": [
        {
          "name": "x",
          "type": "u64"
        }
      ]
    }
  }
],

此方法仅适用于你的程序或客户端初始化或创建的账户,并且具有 IDL,不适用于任意账户。

也就是说,如果你选择 Solana 上的一个随机账户并使用上述代码,反序列化几乎肯定会失败。稍后在本文中,我们将以一种更“原始”的方式读取该账户。

fetch 函数并不是魔法。那么,我们如何对于一个我们没有创建的账户进行操作呢?

从 Anchor Solana 程序创建的账户中提取数据

如果我们知道另一个用 Anchor 创建的程序的 IDL,我们可以方便地读取其账户数据。

让我们在另一个终端中 anchor init 另一个程序,然后让它初始化一个账户,并将该结构中的单个布尔变量设置为 true。我们将这个其他账户称为 other_program,将存放布尔变量的结构称为 TrueOrFalse

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    pub fn setbool(ctx: Context<SetFlag>, flag: bool) -> Result<()> {
        ctx.accounts.true_or_false.flag = flag;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    signer: Signer<'info>,

    system_program: Program<'info, System>,

    #[account(init, payer = signer, space = size_of::<TrueOrFalse>() + 8, seeds=[], bump)]
    true_or_false: Account<'info, TrueOrFalse>,
}

#[derive(Accounts)]
pub struct SetFlag<'info> {
    #[account(mut)]
    true_or_false: Account<'info, TrueOrFalse>, 
}

#[account]
pub struct TrueOrFalse {
    flag: bool,
}

Typescript 代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherProgram } from "../target/types/other_program";

describe("other_program", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("已初始化!", async () => {
    const seeds = []
    const [TrueOrFalse, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    console.log("地址: ", program.programId.toBase58());

    await program.methods.initialize().accounts({trueOrFalse: TrueOrFalse}).rpc();
    await program.methods.setbool(true).accounts({trueOrFalse: TrueOrFalse}).rpc();
  });
});

在另一个终端中运行 against 的本地区块验证器的测试。注意打印出来的 programId。我们需要它来派生 other_program 账户的地址。

读取程序

在另一个终端中,anchor init 另一个程序。我们将其称为 read。我们将仅使用 Typescript 代码来读取 other_programTrueOrFalse 结构,不使用 Rust。这模拟了从另一个程序的存储账户中读取数据。

我们的目录布局如下:

parent_dir/
∟ other_program/
∟ read/

以下代码将从 other_program 中读取 TrueOrFalse 结构。确保:

  • otherProgramAddress 与上述打印的地址匹配
  • 确保你从正确的文件位置读取 other_program.json IDL
  • 确保以 --skip-local-validator 运行测试,以确保此代码读取其他程序创建的账户
import * as anchor from "@coral-xyz/anchor";

describe("read", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

  it("读取其他账户", async () => {
    // 其他程序的 programId - 确保地址正确
    const otherProgramAddress = "4z4dduMSFKFJDnUAKaHnbhHySK8x1PwgArUBXzksjwa8";
    const otherProgramId = new anchor.web3.PublicKey(otherProgramAddress);

    // 加载其他程序的 idl - 确保路径正确
    const otherIdl = JSON.parse(
        require("fs").readFileSync("../other_program/target/idl/other_program.json", "utf8")
    );

    const otherProgram = new anchor.Program(otherIdl, otherProgramId);

    const seeds = []
    const [trueOrFalseAcc, _bump] = 
        anchor.web3.PublicKey.findProgramAddressSync(seeds, otherProgramId);
    let otherStorageStruct = await otherProgram.account.trueOrFalse.fetch(trueOrFalseAcc);

    console.log("flag 的值是:", otherStorageStruct.flag.toString());
  });
});

预期输出如下:output

同样,这仅在其他 Solana 程序是用 Anchor 构建的情况下有效。这是依赖于 Anchor 如何序列化结构。

提取任意账户的数据

在接下来的部分中,我们展示如何在没有 Anchor 魔法的情况下读取数据。

不幸的是,Solana 的 Typescript 客户端文档非常有限,该库已经多次更新,以至于使相关主题的教程过时。

你最好的办法是尝试查找你需要的 Solana web3 Typescript 函数是查阅 HTTP JSON RPC 方法,并寻找一个看起来有希望的方法。在我们看来,getAccountInfo 看起来是个不错的选择(蓝色箭头)。

Solana JSON RPC HTTP 方法

接下来,我们想在 Solana web3 js 中找到该方法。最好使用支持自动补全的 IDE,以便进行尝试直到找到该函数,正如下方视频所示:

https://img.learnblockchain.cn/2025/02/28/file.mp4

接下来我们展示再次运行测试的预期输出:预期输出日志

绿色框中的十六进制 aa 字节显示我们已成功检索存储在 set() 函数中的十进制 170 值。

下一步是解析数据缓冲区,这不是我们在这里要涵盖的内容。

没有强制性的方法来序列化 Solana 账户中的数据。Anchor 以其自身的方式序列化结构,但如果有人用原生 Rust(没有 Anchor)编写 Solana 程序或使用了自己的序列化算法,那么你就必须根据他们序列化数据的方式自定义反序列化算法。

转载:https://learnblockchain.cn/article/11427


Solana基础 - Solana 计数器教程:在账户中读写数据


本教程详细介绍了如何在Anchor框架下向已初始化的Solana账户写入数据,并解释了相关代码的实现原理。

在之前的教程中,我们讨论了如何初始化账户以便将数据持久化存储。本教程将展示如何向我们已初始化的账户写入数据。

以下是之前关于初始化 Solana 账户的教程中的代码。我们添加了一个 set() 函数,用于在 MyStorage 中存储一个数字,并添加了相关的 Set 结构体。

其余代码保持不变:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    // ****************************
    // *** 这个函数是新添加的 ***
    // ****************************
    pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
        ctx.accounts.my_storage.x = new_x;
        Ok(())
    }
}

// **************************
// *** 这个结构体是新添加的 ***
// **************************
##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut, seeds = [], bump)]
    pub my_storage: Account<'info, MyStorage>,
}

##[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = signer,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub signer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

##[account]
pub struct MyStorage {
    x: u64,
}

练习:修改测试,使用参数 170 调用 set()。这是我们尝试持久化存储在 MyStorage 中的 x 的值。你需要在 initialize() 之后调用 set()。别忘了将 170 转换为大数。

set() 函数解释

下面,我们稍微重新排列了代码,将 set() 函数、Set 结构体和 MyStorage 结构体放在一起:set() 函数解释我们现在解释 ctx.accounts.my_storage.x = new_x 的工作原理:

  • ctx 中的 accounts 字段(顶部蓝色框)让我们可以访问 Set 结构体中的所有键。这不是在 Rust 中列出结构体键的方式。accounts 能够引用 Set 结构体中的键,是由于 #[derive(Accounts)] 宏(底部蓝色框)神奇地插入的。
  • my_storage 账户(橙色框)被设置为 mut 或可变的(绿色框),因为我们打算更改其中的值 x(红色框)。
  • my_storage(橙色框)通过将 MyStorage 作为泛型参数传递给 Account,为我们提供了对 MyStorage 账户(黄色框)的引用。我们使用键 my_storage 和存储结构体 MyStorage 是为了可读性,它们不需要是彼此的大小写变体。将它们“绑定在一起”的是黄色框和黄色箭头。

本质上,当调用 set() 时,调用者(Typescript 客户端)将 myStorage 账户传递给 set()。在这个账户内部是存储的地址。在幕后,set 将加载存储,写入 x 的新值,序列化结构体,然后将其存储回去。

SetContext 结构体

set()Context 结构体比 initialize 简单得多,因为它只需要一个资源:对 MyStorage 账户的可变引用。

##[derive(Accounts)]
pub struct Set<'info> {
    #[account(mut, seeds = [], bump)]
    pub my_storage: Account<'info, MyStorage>,
}

回想一下,Solana 交易必须提前指定它将访问哪些账户。set() 函数的结构体指定它将可变地(mut)访问 my_storage 账户。

seeds = []bump 用于派生我们将修改的账户的地址。尽管用户正在为我们传递账户,但 Anchor 通过重新派生地址并将其与用户提供的内容进行比较来验证用户是否传递了该程序真正拥有的账户。

bump 这个术语现在可以视为样板代码。但对于好奇的人来说,它用于确保账户不是加密有效的公钥。这是运行时知道这将用于程序数据存储的方式。

即使我们的 Solana 程序可以自行派生存储账户的地址,用户仍然需要提供 myStorage 账户。这是 Solana 运行时要求的,原因我们将在接下来的教程中讨论。

编写 set 函数的另一种方式

如果我们要向账户写入多个变量,像这样反复写 ctx.accounts.my_storage 会相当笨拙:

ctx.accounts.my_storage.x = new_x;
ctx.accounts.my_storage.y = new_y;
ctx.accounts.my_storage.z = new_z;

相反,我们可以使用 Rust 中的“可变引用”(&mut),它为我们提供了一个“Handle”来操作值。考虑以下对 set() 函数的重写:

pub fn set(ctx: Context<Set>, new_x: u64) -> Result<()> {
    let my_storage = &mut ctx.accounts.my_storage;
    my_storage.x = new_x;

    Ok(())
}

练习:使用新的 set 函数重新运行测试。如果你使用的是本地测试网,别忘了重置验证器。

查看我们的存储账户

如果你正在为测试运行本地验证器,你可以使用以下 Solana 命令行指令查看账户数据:

## 将地址替换为你的测试中的地址
solana account 9opwLZhoPdEh12DYpksnSmKQ4HTPSAmMVnRZKymMfGvn

将地址替换为单元测试中控制台记录的地址。

输出如下:查看我们的存储账户前 8 个字节(绿色框)是判别器。我们的测试在结构体中存储了数字 170,其十六进制表示为 aa,如红色框所示。

当然,命令行不是我们在前端查看账户数据,或者如果我们希望我们的程序查看另一个程序的账户时想要使用的机制。这将在接下来的教程中讨论。

从 Rust 程序中查看我们的存储账户

然而,在 Rust 程序中读取我们自己的存储值非常简单。

我们向 pub mod basic_storage 添加以下函数:

pub fn print_x(ctx: Context<PrintX>) -> Result<()> {
    let x = ctx.accounts.my_storage.x;
    msg!("The value of x is {}", x);
    Ok(())
}

然后我们为 PrintX 添加以下结构体:

##[derive(Accounts)]
pub struct PrintX<'info> {
    pub my_storage: Account<'info, MyStorage>,
}

请注意,my_storage 没有 #[account(mut)] 宏,因为我们不需要它是可变的,我们只是读取它。

然后我们向测试中添加以下行:

await program.methods.printX().accounts({myStorage: myStorage}).rpc();

如果你在后台运行 solana logs,你应该会看到数字被打印出来。

练习:编写一个增量函数,读取 x 并将 x + 1 存储回 x

转载:https://learnblockchain.cn/article/11488


Solana基础 - 在 Solana 和 Anchor 中初始化账户


文章详细介绍了Solana区块链中数据存储的机制,特别是如何通过账户和程序来管理和初始化存储数据。文章通过对比以太坊的存储方式,深入探讨了Solana的存储模型和使用Rust语言进行账户初始化的具体步骤。

迄今为止,我们的所有教程都没有使用“存储变量”或存储任何永久性内容。

在 Solidity 和 Ethereum 中,另一种更为特殊的设计模式是 SSTORE2 或 SSTORE3,其中数据存储在另一个智能合约的字节码中。

在 Solana 中,这并不是一种特殊的设计模式,而是一种常规做法!

回想一下,除非程序被标记为不可变,否则我们可以随意更新 Solana 程序的字节码(如果我们是原始部署者)。

Solana 使用相同的机制来存储数据。

以太坊中的存储槽实际上是一个巨大的键值存储:

{
    key: [smart_contract_address, storage slot]
    value: 32_byte_slot // (例如: 0x00)
}

Solana 的模型类似:它是一个巨大的键值存储,其中“键”是一个 base 58 编码的地址,而值是一个可以大到 10MB 的数据块(或可选择不存储任何内容)。它可以这样可视化:

{
    // key 是一个 base58 编码的 32 字节序列
    key: ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs
    value: {
        data: 020000006ad1897139ac2bdb67a3c66a...
        // 其他字段省略
    }
}

在 Ethereum 中,一个智能合约的字节码和存储变量是分别存储的,即它们是不同索引的,必须使用不同的 API 加载。

以下图表展示了以太坊如何维护状态。每个帐户都是 Merkle 树中的一个叶子。请注意,“存储变量”被存储在智能合约的帐户(帐户 1)“内部”。Ethereum Storage在 Solana 中,一切都是帐户,可以潜在地保存数据。有时我们将一个帐户称为“程序帐户”,另一个帐户称为“存储帐户”,但唯一的区别在于可执行标志是否设置为真,以及我们打算如何使用帐户的数据字段。

下面,我们可以看到 Solana 存储是一个从 Solana 地址到帐户的巨大键值存储:Solana Accounts

想象一下,如果以太坊没有存储变量,智能合约默认是可变的。要存储数据,你必须创建其他“智能合约”,并将数据保存在它们的字节码中,然后在必要时修改它。这是 Solana 的一种心理模型。

另一种心理模型是 Unix 中的一切都是文件,有些文件是可执行的。可以将 Solana 帐户视为文件。它们保存内容,但它们也具有元数据,指示谁拥有该文件,它是否可执行,等等。

在 Ethereum 中,存储变量直接与智能合约耦合。除非智能合约通过公共变量、delegatecall 或某些设置方法授予读写访问权限,否则存储变量默认只能由单个合约写入或读取(尽管任何人都可以从链下读取存储变量)。在 Solana 中,所有“存储变量”可以被任何程序读取,但只有其所有者程序可以写入它。

存储与程序“绑定”的方式是通过所有者字段。

在下图中,我们看到帐户 B 是由程序帐户 A 所拥有。我们知道 A 是程序帐户,因为“可执行”设置为 true。这表明 B 的数据字段将存储 A 的数据:program storage for solana

Solana 程序必须在使用之前初始化

在 Ethereum 中,我们可以直接写入一个我们之前未使用的存储变量。然而,Solana 程序需要一个显式的初始化事务。也就是说,我们必须先创建帐户,然后才能向其中写入数据。

可以在一个事务中初始化并写入一个 Solana 帐户——然而这会引入安全问题,这将使讨论变得复杂。如果我们现在处理它,暂时只需说 Solana 帐户必须在使用之前进行初始化。

基本存储示例

让我们将以下 Solidity 代码翻译为 Solana:

contract BasicStorage {
    Struct MyStorage {
        uint64 x;
    }

    MyStorage public myStorage;

    function set(uint64 _x) external {
        myStorage.x = _x;
    }
} 

将单个变量包裹在一个结构中可能看起来很奇怪。

但在 Solana 程序中,特别是在 Anchor 中,所有存储,或者说帐户数据,都被视为结构。原因在于帐户数据的灵活性。由于帐户是可以非常大的数据块(最大可达 10MB),我们需要某种“结构”来解读数据,否则它只是一个毫无意义的字节序列。

在后台,Anchor 在我们尝试读取或写入数据时会对帐户数据进行反序列化和序列化为结构。

如上所述,我们需要在使用 Solana 帐户之前对其进行初始化,因此在实现 set() 函数之前,我们需要编写 initialize() 函数。

帐户初始化样板代码

让我们创建一个名为 basic_storage 的新 Anchor 项目。

以下是我们编写的最小代码,以初始化一个仅保存一个数字 xMyStorage 结构。 (请参见底部代码中的结构 MyStorage):

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = signer,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub signer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[account]
pub struct MyStorage {
    x: u64,
}

1) initialize 函数

请注意,initialize() 函数中没有代码——实际上它所做的只是返回 Ok(())

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
}

初始化帐户的函数并不一定必须为空,我们可以有自定义逻辑。但在我们的示例中,它是空的。用于“初始化”的函数不必命名为 initialize,但这个名字是有帮助的。

2) Initialize 结构

Initialize 结构包含初始化帐户所需资源的引用:

  • my_storage: 我们正在初始化的 MyStorage 类型的结构。
  • signer: 负责为结构的存储支付“Gas费”的钱包。 (关于存储的Gas费用将在后面讨论)。
  • system_program: 我们将在本教程稍后讨论。Annotated Initialize struct

'info 关键字是一个 Rust 生命周期。这是一个重要的主题,目前我们最好将其视为样板代码。

我们将专注于 my_storage 上方的宏,因为这就是初始化工作的地方。

3) Initialize 结构中的 my_storage 字段

位于 my_storage 字段上方的属性宏(紫色箭头)是 Anchor 知道此事务旨在初始化此帐户的方式(请记住,属性宏以 # 开头,并通过额外功能增强结构):annotation of struct fields

这里重要的关键字是 init

当我们 init 一个帐户时,必须提供额外的信息:

  • payer(蓝框):谁在为分配存储支付 SOL。签名者被指定为 mut,因为他们的帐户余额将会变化,即会从他们的帐户中扣除一些 SOL。因此,我们将他们的帐户注释为“可变”。
  • space(橙框):这指示帐户将占用多少空间。我们可以使用 std::mem::size_of 工具,并将我们正在尝试存储的结构 MyStorage(绿色框)作为参数。+ 8(粉色框)的含义将在下一点中讨论。
  • seedsbump(红框):一个程序可以拥有多个帐户,它通过“种子”在帐户之间进行“区分”,该种子用于计算“鉴别符”。“鉴别符”占 8 个字节,这就是为什么除了我们结构所占的空间外还需要分配额外的 8 个字节。bump 目前可以视为样板代码。

这可能听起来有点复杂,但不用担心。初始化帐户在很大程度上可以视为样板代码。

4) 什么是系统程序?

system program 是一个内置于 Solana 运行时的程序(有点像 Ethereum 预编译),它从一个帐户向另一个帐户转移 SOL。我们将在后面的教程中重新访问这个概念。现在,我们需要将 SOL 从支付 MyStruct 存储的签名者那里转移,因此 system program 总是初始化事务的一部分。

5) MyStorage 结构

回想一下 Solana 帐户内的数据字段:data highlighted in solana account在幕后,这是一串字节序列。上面示例中的结构:

#[account]
pub struct MyStorage {
    x: u64,
}

在写入时被序列化为字节序列并存储在 data 字段中。在写入期间,data 字段根据该结构被反序列化。

在我们的示例中,我们仅使用了结构中的一个变量,尽管如果我们想的话,可以添加更多变量或其他类型的变量。

Solana 运行时并不强制我们使用结构来存储数据。从 Solana 的角度来看,帐户只是保存数据块。但是,Rust 有很多方便的库将结构转换为数据块和反之亦然,因此结构是惯例。Anchor 在幕后利用这些库。

你不需要使用结构来使用 Solana 帐户。可以直接写入字节序列,但这不是存储数据的便捷方式。

#[account] 宏透明地实现了所有魔法。

6) 单元测试初始化

以下 Typescript 代码将运行上述 Rust 代码。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";

describe("basic_storage", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const seeds = []
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    console.log("the storage account address is", myStorage.toBase58());

    await program.methods.initialize().accounts({ myStorage: myStorage }).rpc();
  });
});

这是单元测试的输出:solana account initialize test passing我们将在后面的教程中学习更多,但 Solana 要求我们提前指定一笔交易将与哪些帐户交互。由于我们正在与存储 MyStruct 的帐户交互,因此我们需要提前计算其“地址”,并将其传递给 initialize() 函数。这可以通过以下 Typescript 代码来完成:

seeds = []
const [myStorage, _bump] = 
    anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

请注意,seeds 是一个空数组,就像它在 Anchor 程序中一样。

预测 Solana 中的帐户地址就像以太坊中的 create2

在以太坊中,使用 create2 创建的合约地址依赖于:

  • 部署合约的地址
  • 一个盐值
  • 以及创建合约的字节码

预测 Solana 中初始化帐户的地址则非常相似,只是忽略了“字节码”。具体而言,它依赖于:

  • 拥有存储帐户的程序 basic_storage(这类似于部署合约的地址)
  • 以及 seeds(这类似于 create2 的“盐”)

在本教程中的所有示例中,seeds 是一个空数组,但我们将在后面的教程中探讨非空数组。

不要忘记将 my_storage 转为 myStorage

Anchor 将 Rust 的蛇形命名法默默转换为 Typescript 的驼峰命名法。当我们在 Typescript 中向 initialize 函数提供 .accounts({myStorage: myStorage}) 时,它是在“填写” Rust 中 Initialize 结构的 my_storage 键(下方绿色圆圈)。system_programSigner 会由 Anchor 静默填充:snake case to camel case conversions

帐户不能被初始化两次

如果我们可以重新初始化一个帐户,那将非常麻烦,因为用户可能会清除系统中的数据!幸运的是,Anchor 在后台对此进行了防护。

如果你在第二次运行测试(不重置本地验证器)时会得到下图中的错误。

或者,如果你不使用本地验证器,可以运行以下测试:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";

describe("basic_storage", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("Is initialized!", async () => {
    const seeds = []
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    // ********************************************
    // **** 请注意,我们调用了初始化两次 ****
    // ********************************************
    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
  });
});

当我们运行测试时,测试失败,因为第二次调用 initialize 抛出了错误。预期输出如下所示:solana account cannot be initialized twice

不要忘记在多次运行测试时重置验证器

因为 solana-test-validator 会仍然记住第一次单元测试中的帐户,所以你需要使用 solana-test-validator --reset 在测试之间重置验证器。否则,你会遇到上述错误。

初始化帐户的总结

对大多数 EVM 开发者来说,初始化帐户的必要性可能感觉不自然。

别担心,你会不断看到这一代码序列,它会随着时间的推移而成为你的第二天性。

在本教程中,我们只考虑了初始化存储,在接下来的教程中,我们将学习读取、写入和删除存储。将有很多机会让你直观地了解我们今天查看的所有代码的作用。

练习: 修改 MyStorage 以保存 xy,就像它是一个笛卡尔坐标。这意味着在 MyStorage 结构中添加 y 并将它们的类型从 u64 改为 i64。你无需修改代码的其他部分,因为 size_of 会为你重新计算大小。确保重置验证器,以便原始存储帐户被擦除,从而不阻止你再次初始化该帐户。

转载: https://learnblockchain.cn/article/11402