本文介绍了Solana区块链中内置的多调用(multicall)功能,以及如何使用Anchor框架在Solana上进行批量交易。文章还详细解释了Solana交易大小限制,并展示了如何使用Rust和TypeScript代码实现原子性批量交易。
在以太坊中,如果我们想要原子地批量处理多个交易,我们使用多重调用模式。如果其中一个失败,其余的也会失败。
Solana 在运行时内置了这一功能,因此我们不需要实现多重调用。在下面的示例中,我们在一次交易中初始化一个账户并写入它——无需使用 init_if_needed
。
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";
describe("batch", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Batch as Program<Batch>;
it("Is initialized!", async () => {
const wallet = anchor.workspace.Batch.provider.wallet.payer;
const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
const initTx = await program.methods.initialize().accounts({pda: pda}).transaction();
// 对于 u32,我们不需要使用大数
const setTx = await program.methods.set(5).accounts({pda: pda}).transaction();
let transaction = new anchor.web3.Transaction();
transaction.add(initTx);
transaction.add(setTx);
await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);
const pdaAcc = await program.account.pda.fetch(pda);
console.log(pdaAcc.value); // 输出 5
});
});
以下是对应的 Rust 代码:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");
##[program]
pub mod batch {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
ctx.accounts.pda.value = new_val;
Ok(())
}
}
##[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = size_of::<PDA>() + 8, seeds = [], bump)]
pub pda: Account<'info, PDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
##[derive(Accounts)]
pub struct Set<'info> {
#[account(mut)]
pub pda: Account<'info, PDA>,
}
##[account]
pub struct PDA {
pub value: u32,
}
关于上面代码的一些评论:
u32
值或更小值传递给 Rust 时,我们不需要使用 Javascript 大数。await program.methods.initialize().accounts({pda: pda}).rpc()
,而是使用 await program.methods.initialize().accounts({pda: pda}).transaction()
来创建一个交易。Solana 交易的总大小不能超过 1232 字节。
这意味着你无法批量处理“无限”数量的交易并支付更多 gas,就像在以太坊中那样。
让我们修改 Rust 中的 set
函数以始终失败。这将帮助我们看到,如果其中一个后续批处理交易失败,initialize
交易会被回滚。
以下 Rust 程序在调用 set
时总是返回错误:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE");
##[program]
pub mod batch {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn set(ctx: Context<Set>, new_val: u32) -> Result<()> {
ctx.accounts.pda.value = new_val;
return err!(Error::AlwaysFails);
}
}
##[error_code]
pub enum Error {
#[msg(always fails)]
AlwaysFails,
}
##[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = size_of::<PDA>() + 8, seeds = [], bump)]
pub pda: Account<'info, PDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
##[derive(Accounts)]
pub struct Set<'info> {
#[account(mut)]
pub pda: Account<'info, PDA>,
}
##[account]
pub struct PDA {
pub value: u32,
}
以下 Typescript 代码发送初始化和设置的批处理交易:
import * as anchor from "@coral-xyz/anchor";
import { Program, SystemProgram } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";
describe("batch", () => {
// 配置客户端以使用本地集群。
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Batch as Program<Batch>;
it("Set the number to 5, initializing if necessary", async () => {
const wallet = anchor.workspace.Batch.provider.wallet.payer;
const [pda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
// 输出 pda 的地址
console.log(pda.toBase58());
let transaction = new anchor.web3.Transaction();
transaction.add(await program.methods.initialize().accounts({pda: pda}).transaction());
transaction.add(await program.methods.set(5).accounts({pda: pda}).transaction());
await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);
});
});
当我们运行测试,然后查询本地验证器以获取 pda 账户时,我们发现它不存在。即使初始化交易在前,随后的设置交易的回滚导致整个交易被取消,因此没有账户被初始化。
你可以使用前端代码模拟 init_if_needed
的行为,同时拥有一个单独的 initialize
函数。然而,从用户的角度来看,他们在第一次使用账户时无需发出多个交易。
要确定一个账户是否需要初始化,我们检查它是否有零 lamports 或被系统程序拥有。以下是如何在 Typescript 中实现此功能:
import * as anchor from "@coral-xyz/anchor";
import { Program, SystemProgram } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";
describe("batch", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Batch as Program<Batch>;
it("Set the number to 5, initializing if necessary", async () => {
const wallet = anchor.workspace.Batch.provider.wallet.payer;
const [pda, _bump]...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!