在之前的教程中,seeds=[] 参数始终是空的。如果我们在其中放入数据,它的行为就会像 Solidity 映射(mapping)中的一个或多个键(key)。
考虑以下示例:
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 中的项需要是字节(bytes)类型。然而,我们传入的是 u64,它并不是字节类型。为了将其转换为字节,我们使用了 to_le_bytes()。这里的 “le” 代表“小端序(little endian)”。seeds 并不强制要求编码为小端序字节,我们只是在这个例子中这样选择。只要保持一致,大端序(Big endian)也是可以的。如果要转换为大端序,我们需要使用 to_be_bytes()。
#[instruction(key: u64)]
为了在 initialize(ctx: Context<Initialize>, key: u64) 中“传递”函数参数 key,我们需要使用 instruction 宏,否则我们的 init 宏将无法“看到”来自 initialize 的 key 参数。
初始化映射: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 字节的字节缓冲区(bytes buffer)。我们选择 8 字节是因为我们的键是 64 位,而 64 位等于 8 字节。“le” 表示小端序,以便与我们的 Rust 代码匹配。
映射中的每个“值(value)”都是一个独立的账户,必须单独进行初始化。
设置映射:Rust
设置该值所需的额外 Rust 代码。这里的所有语法应该都很熟悉了。
// inside the #[program] module
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();
// set the account
await program.methods.set(key, value).accounts({val: valueAccount}).rpc();
// read the account back
let result = await program.account.val.fetch(valueAccount);
console.log(`the value ${result.value} was stored in ${valueAccount.toBase58()}`);
});
});
澄清 “nested mappings”(嵌套映射)
在 Python 或 JavaScript 等语言中,真正的嵌套映射是一个指向另一个哈希映射(hashmap)的哈希映射。
然而在 Solidity 中,“nested mappings” 只是一个具有多个键的单一映射,这些键的表现就像它们是一个组合键一样。
在“真正的”嵌套映射中,你可以只提供第一个键,并获得返回给你的另一个哈希映射。
Solidity 的 “nested mappings” 并不是“真正的”嵌套映射:你不能只提供一个键然后得到一个返回的映射,你必须提供所有的键才能得到最终结果。
如果你使用 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)] // new key args added
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)] // new key args added
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 () => {
// we now have two keys
const key1 = new anchor.BN(42);
const key2 = new anchor.BN(43);
const value = new anchor.BN(1337);
// seeds has two values
const seeds = [key1.toArrayLike(Buffer, "le", 8), key2.toArrayLike(Buffer, "le", 8)];
let valueAccount = anchor.web3.PublicKey.findProgramAddressSync(
seeds,
program.programId
)[0];
// functions now take two keys
await program.methods.initialize(key1, key2).accounts({val: valueAccount}).rpc();
await program.methods.set(key1, key2, value).accounts({val: valueAccount}).rpc();
// read the account back
let result = await program.account.val.fetch(valueAccount);
console.log(`the value ${result.value} was stored in ${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 代码,创建一个具有两个映射的程序:第一个映射包含单一键,第二个映射包含两个键。思考一下在指定第一个映射时,如何将两级映射转换为单级映射。
通过 RareSkills 学习 Solana
请查看我们的 Solana 课程 以获取其余的 Solana 教程。
最初发布于 2024 年 2 月 27 日