Solana tiene multicall integrado
En Ethereum, utilizamos el patrón multicall si queremos agrupar múltiples transacciones juntas de forma atómica. Si una falla, el resto falla.
Solana tiene esto integrado en el runtime, por lo que no necesitamos implementar un multicall. En el siguiente ejemplo, inicializamos una cuenta y escribimos en ella en una sola transacción — sin usar 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();
// for u32, we don't need to use big numbers
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); // prints 5
});
});
Aquí está el código Rust correspondiente:
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,
}
Algunos comentarios sobre el código anterior:
- Al pasar un valor
u32o menor a Rust, no necesitamos usar un bignumber de Javascript. - En lugar de hacer
await program.methods.initialize().accounts({pda: pda}).rpc()hacemosawait program.methods.initialize().accounts({pda: pda}).transaction()para crear una transacción.
Límite de tamaño de transacción en Solana
El tamaño total de una transacción en Solana no puede exceder los 1232 bytes.
La implicación de esto es que no podrás agrupar un número “ilimitado” de transacciones y simplemente pagar más gas como lo harías en Ethereum.
Demostrando la atomicidad de las transacciones agrupadas
Vamos a alterar nuestra función set en Rust para que siempre falle. Esto nos ayudará a ver que la transacción initialize se revierte si una de las transacciones agrupadas subsiguientes falla.
El siguiente programa en Rust siempre devuelve un error cuando se llama a 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,
}
El siguiente código Typescript envía una transacción agrupada de inicialización y set:
import * as anchor from "@coral-xyz/anchor";
import { Program, SystemProgram } from "@coral-xyz/anchor";
import { Batch } from "../target/types/batch";
describe("batch", () => {
// Configure the client to use the local cluster.
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);
// console.log the address of the 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]);
});
});
Cuando ejecutamos la prueba, y luego consultamos al validador local por la cuenta pda, vemos que no existe. Aunque la transacción initialize fue primero, la transacción set que vino después se revirtió, causando que toda la transacción fuera cancelada, y por lo tanto ninguna cuenta fue inicializada.

“Init if needed” en el frontend
Puedes simular el comportamiento de init_if_needed usando código frontend mientras tienes una función initialize separada. Sin embargo, desde la perspectiva del usuario, todo esto se simplificará, ya que no tienen que emitir múltiples transacciones al usar una cuenta por primera vez.
Para determinar si una cuenta necesita ser inicializada, verificamos si tiene cero lamports o si es propiedad del system program. Así es como podemos hacerlo en 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);
let accountInfo = await anchor.getProvider().connection.getAccountInfo(pda);
let transaction = new anchor.web3.Transaction();
if (accountInfo == null || accountInfo.lamports == 0 || accountInfo.owner == anchor.web3.SystemProgram.programId) {
console.log("need to initialize");
const initTx = await program.methods.initialize().accounts({pda: pda}).transaction();
transaction.add(initTx);
}
else {
console.log("no need to initialize");
}
// we're going to set the number anyway
const setTx = await program.methods.set(5).accounts({pda: pda}).transaction();
transaction.add(setTx);
await anchor.web3.sendAndConfirmTransaction(anchor.getProvider().connection, transaction, [wallet]);
const pdaAcc = await program.account.pda.fetch(pda);
console.log(pdaAcc.value);
});
});
También necesitamos modificar nuestro código Rust para no fallar forzosamente en la operación 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;
Ok(()) // ERROR HAS BEEN REMOVED
}
}
#[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,
}
Si ejecutamos la prueba dos veces contra la misma instancia del validador local, obtendremos los siguientes resultados:
Primera ejecución de prueba:

Segunda ejecución de prueba:

¿Cómo despliega Solana programas de más de 1232 bytes?
Si creas un nuevo programa en Solana y ejecutas run anchor deploy (o anchor test) verás en los logs que hay numerosas transacciones hacia el BFPLoaderUpgradeable:
Transaction executed in slot 65695:
Signature: 62Zu3NPyjjaEoH4XSc7kULtuoszLPctM1PTrLiC7A3CiaGJEzYscQ5c9SKbN3UUoqctyrdzW2upDXnSC4VnMjyfZ
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
Signature: 3cD19SGmdfd991NjcGHpYcnjhZ3FYqEWnHMJALQ95X5fvwHVhB3Cw9PwqSDwziiCMQHcZ8iuxXqg3UDJmp7gJHd3
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
Signature: 5apuTjqCMKGdyYGRZ9sCLDapPCKqjyJMyqWMC24EsW4pLzHhM3YUgnf5Q2sqXSLVTxjKaSgZ3fcCkZrAah32uzh2
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
Signature: HJ8XaErydn8ojxaEknZsg43pGA9mC8TBqV4zwSrZgXFvi5UqgZjNU65TQKqb6DyEZFtHecytt1k7U4N9Vw52rur
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
Signature: 3uY9beX23VdRXeEqUSP4cpAuTevdcjHDZ8K3pwKVpw51mwX1jLGQ7LYB7d68dWSe571TeAoxq33eoUU7c8gTDgic
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65695:
Signature: 666r5LcQaH1ZcZWhrHFUFEqjHXEE1QUyh27HFRkWsDQihM7FYtyz3v4eJgVkQwhJuMDSYHJZHDRrSsNVbCFrEkV9
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
Signature: 2QmPZFkDN9WsKiNjHFdaNLuaYbQFXtN8yRgHTDC3Ce2z28483LNVyuE1AnwgsRisiKeiKe5Wu9WTbkTbAwmodPTC
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
Signature: EsTiuCn6PGA158Xi43XwGtYf2tDJTbgxRJehHS9AQ9AcW4qraxWuNPzdD7Wk4yeL65oaaa1G8WMqkjYbJcGzhv1V
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
Signature: 3PZSv4dnggW52C3FL9E1JPvwueBp7E342o9aM29mH2CnfGsGLDBRJcN64EQeJEkc57hgGyZsiz8J1fSV1Qquz8zx
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65696:
Signature: 4ynMY9ioELf4xxtBpHeM1q2fuWM5usa1w8dXQhLhjstR8U6LmpYHTJs7Gc82XkVyMXywPrsbu3EDCAcpoFj7qwkJ
Status: Ok
Log Messages:
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 65698:
Signature: 5rs38HHbWF2ZrsgDCux1X9FRvkrhTdrEimdhidd2EYbaeezAmy9Tv5AFULgsarPtJCft8uZmsvhpYKwHGxnLf2sG
Status: Ok
Log Messages:
Program 11111111111111111111111111111111 invoke [1]
Program 11111111111111111111111111111111 success
Program BFPLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Deployed program Ao9LdZtHdMAzrFUEfRNbKEb5H4nXvpRZC69kxeAGbTPE
Program BFPLoaderUpgradeab1e11111111111111111111111 success
Aquí, Anchor está dividiendo el despliegue del bytecode en múltiples transacciones porque desplegar todo el bytecode de una vez no cabría en una sola transacción. Podemos ver cuántas transacciones tomó dirigiendo los logs hacia el archivo y contando el número de transacciones que ocurrieron:
solana logs > logs.txt
# run `anchor deploy` in another shell
grep "Transaction executed" logs.txt | wc -l
Esto coincidirá aproximadamente con lo que aparece temporalmente después del comando anchor test o anchor deploy:

El proceso exacto de cómo se agrupan las transacciones se describe en la Documentación de Solana: Cómo funciona el despliegue de programas en Solana.
La lista de transacciones son transacciones separadas, no una transacción agrupada. Si estuvieran agrupadas, excederían el límite de 1232 bytes.
Aprende más con RareSkills
Consulta nuestro curso de desarrollo en Solana para ver más tutoriales de Solana.
Publicado originalmente el 10 de marzo de 2024