您正在查看: Solana-优秀转载 分类下的文章

Solana基础 - 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因

文章详细介绍了 Solana 区块链中缺少的一些功能,如 fallback 和 receive 函数、view 和 pure 函数,以及 Solidity 中的自定义修饰符和单位在 Rust 中的缺失。此外,文章还讨论了 Solana 中读取账户数据的方式和防止重入攻击的策略。

Solana 没有 fallback 或 receive 函数

Solana 交易必须预先指定它将修改或读取的账户作为交易的一部分。如果“fallback”函数访问了一个不确定的账户,那么整个交易将失败。这将使用户不得不预测 fallback 函数将访问的账户。因此,直接不允许这种类型的函数更为简单。

Solana 没有“view”或“pure”函数的概念

Solidity 中的“view”函数通过两种机制保证状态不会改变:

  • view 函数中的所有外部调用都是 静态调用(如果发生状态更改,调用将回滚)
  • 如果编译器检测到状态更改的操作码,它将抛出错误

纯函数更进一步,编译器会检查是否存在查看状态的操作码。

这些函数限制主要发生在编译器层面,而 Anchor 没有实现任何这些编译器检查。Anchor 并不是构建 Solana 程序的唯一框架。Seahorse 是另一个框架。也许其他框架会带有明确说明函数可以做什么和不能做什么的函数注解,但目前我们可以依赖以下保证:如果某个账户未包含在 Context 结构体定义中,该函数将不会访问该账户。

并不意味着该账户完全无法被访问。例如,我们可以编写一个单独的程序来读取账户,并以某种方式将该数据转发给相关函数。

最后,Solana 虚拟机或运行时中没有 staticcall 这样的东西。

无论如何,view 函数在 Solana 中并不必要

因为 Solana 程序可以读取传递给它的任何账户,所以它可以读取另一个程序拥有的账户。

拥有账户的程序不需要实现 view 函数来允许另一个程序查看该账户。web3 js 客户端或其他程序可以直接查看 Solana 账户数据

这有一个非常重要的含义:

无法使用重入锁来直接防御 Solana 中的只读重入。程序必须为读者暴露一个标志,以知道数据是否可靠。

只读重入发生在受害合约访问显示被操纵数据的 view 函数时。在 Solidity 中,可以通过在 view 函数中添加 nonReentrant 修饰符来防御。然而,在 Solana 中,无法阻止另一个程序查看账户中的数据。

然而,Solana 程序仍然可以实现重入防护使用的标志。消费另一个程序账户的程序可以检查这些标志,以查看账户是否可能处于重入状态,从而不应被信任。

Rust 中没有自定义修饰符

onlyOwnernonReentrant 这样的自定义修饰符是 Solidity 的构造,而不是 Rust 中可用的功能。

Rust 或 Anchor 中不可用自定义单位

由于 Solidity 与以太坊紧密相关,它有方便的像 etherswei 这样的关键字来衡量以太币。不出所料,LAMPORTS_PER_SOL 在 Rust 中没有定义,但让人有些惊讶的是,它在 Anchor Rust 框架中也没有定义。不过,它在 Solana web3 js 库中是可用的。

同样,Solidity 中有 days 作为 84,600 秒的方便别名,但在 Rust/Anchor 中没有这样的等价物。

Solana 中没有所谓的“payable”函数。程序从用户那里转移 SOL,用户不将 SOL 转移给程序

这是下一个教程的主题。

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

Solana基础 - 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance

文章介绍了如何在Solana程序中使用Anchor框架读取账户余额,并详细解释了UncheckedAccount的使用及其安全性考虑。

在Anchor Rust中读取账户余额

要在Solana程序中读取地址的Solana余额,请使用以下代码:

use anchor_lang::prelude::*;

declare_id!("Gnf6u7S7fGJbqEGH9PuDE5Prq6f6ZrDxHY3jNJ4SYySQ");

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

    pub fn read_balance(ctx: Context<ReadBalance>) -> Result<()> {
        let balance = ctx.accounts.acct.to_account_info().lamports();

        msg!("余额以Lamports表示为 {}", balance);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct ReadBalance<'info> {
    /// CHECK: 尽管我们读取这个账户的余额,但我们并没有用到这个信息
    pub acct: UncheckedAccount<'info>,
}

下面是触发它的web3 js代码:

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

describe("balance", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

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

  // 以下是我们使用的Solana钱包
  let pubkey = new anchor.web3.PublicKey("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj");


  it("测试余额", async () => {
    const tx = await program.methods.readBalance().accounts({ acct: pubkey }).rpc();
  });
});

本示例中的某些项与以前的教程不同,特别是使用UncheckedAccount

在Solana Anchor中什么是UncheckedAccount

UncheckedAccount类型通知Anchor不检查正在读取的账户是否被该程序拥有。

注意,我们通过Context结构传递的账户并不是该程序初始化的账户,因此该程序并不拥有它。

当Anchor读取#[derive(Accounts)]中的Account类型账户时,它会检查(在后台)该账户是否被该程序拥有。如果不拥有,执行将会停止。

这作为一个重要的安全检查。

如果恶意用户构造了一个该程序未创建的账户,并将其传递给Solana程序,而Solana程序盲目信任该账户中的数据,就可能发生严重错误。

例如,如果该程序是一个银行,而该账户存储了用户的余额,那么黑客可以提供一个不同的账户,其人工作高于实际余额。

不过,要实施这个黑客,用户必须在单独的交易中创建虚假账户,然后将其传递给Solana程序。然而,Anchor框架会在后台检查该账户是否不被程序拥有,并拒绝读取该账户。

UncheckedAccount绕过了这个安全检查。

重要AccountInfoUncheckedAccount是彼此的别名,并且AccountInfo具有相同的安全性考虑。

在我们的案例中,我们传递的账户显然不被程序拥有——我们想检查一个任意账户的余额。因此,我们必须确保移除这个安全检查后没有关键逻辑可以被篡改。

在我们的案例中,我们只是将余额记录到控制台,但大多数现实案例会有更复杂的逻辑。

什么是/// CHECK:

由于使用UncheckedAccount的危险性,Anchor强制你包括此注释,以鼓励你不要忽视安全考虑。

练习:删除/// CHECK:注释并运行anchor build,你应该看到构建停止,并要求你添加回注释并解释为什么未经检查的账户是安全的。也就是说,读取不可信的账户可能是危险的,Anchor希望确保你不对账户中的数据执行任何关键操作。

为什么程序中没有#[account]结构?

#[account]结构告诉Anchor如何反序列化持有数据的账户。例如,以下看似的账户结构将通知Anchor将存储在账户中的数据反序列化为一个单一的u64

#[account]
pub struct Counter {
    counter: u64
}

然而,在我们的案例中,我们并不想从账户中读取数据——我们只想读取余额。这类似于我们可以读取以太坊地址的余额,而无法读取其代码。由于我们想反序列化数据,因此我们不提供#[account]结构。

并非所有账户中的SOL都是可支配的

回忆一下我们关于Solana账户租金的讨论,账户必须保持一定的SOL余额以便“免租”,否则运行时将删除该账户。仅仅因为账户中有“1 SOL”并不一定意味着该账户可以支配全部1 SOL。

例如,如果你正在构建一个质押或银行应用程序,其中用户存入的SOL存放在单独的账户中,那么简单地测量这些账户的SOL余额并不准确,因为租金将包含在余额中。

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

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

搜索