Solana 中的多重调用:批量交易与交易大小限制

  • RareSkills
  • 发布于 2024-03-12 14:56
  • 阅读 221

本文介绍了Solana区块链中内置的多调用(multicall)功能,以及如何使用Anchor框架在Solana上进行批量交易。文章还详细解释了Solana交易大小限制,并展示了如何使用Rust和TypeScript代码实现原子性批量交易。

Hero image showing Solona transaction batch and transaction size limit

Solana 内置了多重调用(multicall)

在以太坊中,如果我们想要原子地批量处理多个交易,我们使用多重调用模式。如果其中一个失败,其余的也会失败。

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 交易大小限制

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 账户时,我们发现它不存在。即使初始化交易在前,随后的设置交易的回滚导致整个交易被取消,因此没有账户被初始化。

Error : Initialize will get rolled back because set will fail

前端的“需要初始化”功能

你可以使用前端代码模拟 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]...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/