Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/magicblock-labs/magicblock-engine-examples/llms.txt

Use this file to discover all available pages before exploring further.

Once accounts are delegated to an Ephemeral Rollup, you can execute transactions with minimal latency (typically 50-100ms). Transactions in ERs work identically to Solana transactions, but connect to the ER endpoint instead of the base layer.

Overview

Transactions in Ephemeral Rollups:
  • Execute on delegated accounts with sub-100ms latency
  • Use the same instruction format as Solana
  • Support all program types (Anchor, native Rust, Bolt, etc.)
  • Automatically commit to the base layer at configured intervals
  • Can be manually committed or undelegated at any time

Connection setup

To execute transactions on an ER, create a connection to the ER endpoint instead of the base layer.

Anchor connection

From the anchor-counter example:
/home/daytona/workspace/source/anchor-counter/tests/anchor-counter.ts
import * as anchor from "@coral-xyz/anchor";

// Base layer connection (Solana)
const provider = anchor.AnchorProvider.env();

// Ephemeral Rollup connection
const providerEphemeralRollup = new anchor.AnchorProvider(
  new anchor.web3.Connection(
    process.env.EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app/",
    {
      wsEndpoint:
        process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/",
    }
  ),
  anchor.Wallet.local()
);

console.log("Base Layer:", provider.connection.rpcEndpoint);
console.log("Ephemeral Rollup:", providerEphemeralRollup.connection.rpcEndpoint);

Web3.js connection

From the rust-counter example:
/home/daytona/workspace/source/rust-counter/tests/web3js/rust-counter.test.ts
import { Connection } from "@solana/web3.js";

// Base layer connection
const connectionBaseLayer = new Connection(
  process.env.PROVIDER_ENDPOINT || "https://api.devnet.solana.com",
  { wsEndpoint: process.env.WS_ENDPOINT || "wss://api.devnet.solana.com" }
);

// Ephemeral Rollup connection
const connectionEphemeralRollup = new Connection(
  process.env.EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app/",
  { wsEndpoint: process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/" }
);
You need both connections: one for base layer operations (delegation/undelegation) and one for ER execution.

Environment configuration

Set these environment variables to configure your ER endpoints:
export EPHEMERAL_PROVIDER_ENDPOINT="https://devnet-as.magicblock.app/"
export EPHEMERAL_WS_ENDPOINT="wss://devnet-as.magicblock.app/"

Executing transactions

Transactions on ERs follow the same pattern as Solana, but use the ER connection.

Anchor transactions

From the anchor-counter test:
it("Increase counter on ER", async () => {
  const start = Date.now();
  
  // Build the transaction
  let tx = await program.methods
    .increment()
    .accounts({
      counter: counterPDA,
    })
    .transaction();
  
  // Set fee payer and recent blockhash from ER
  tx.feePayer = providerEphemeralRollup.wallet.publicKey;
  tx.recentBlockhash = (
    await providerEphemeralRollup.connection.getLatestBlockhash()
  ).blockhash;
  
  // Sign with ER wallet
  tx = await providerEphemeralRollup.wallet.signTransaction(tx);
  
  // Send to ER
  const txHash = await providerEphemeralRollup.sendAndConfirm(tx);
  
  const duration = Date.now() - start;
  console.log(`${duration}ms (ER) Increment txHash: ${txHash}`);
  // Typical output: "87ms (ER) Increment txHash: ..."
});

Native Web3.js transactions

From the rust-counter test:
import { Transaction, TransactionInstruction, sendAndConfirmTransaction } from "@solana/web3.js";
import * as borsh from "borsh";

it("Increase counter on ER", async () => {
  const start = Date.now();
  
  // Create transaction
  const tx = new Transaction();
  
  // Define accounts
  const keys = [
    {
      pubkey: userKeypair.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: counterPda,
      isSigner: false,
      isWritable: true,
    },
  ];
  
  // Serialize instruction data
  const serializedInstructionData = Buffer.concat([
    Buffer.from(CounterInstruction.IncreaseCounter, "hex"),
    borsh.serialize(IncreaseCounterPayload.schema, new IncreaseCounterPayload(1)),
  ]);
  
  // Create instruction
  const incrementIx = new TransactionInstruction({
    keys: keys,
    programId: PROGRAM_ID,
    data: serializedInstructionData,
  });
  
  tx.add(incrementIx);
  
  // Send to ER
  const txHash = await sendAndConfirmTransaction(
    connectionEphemeralRollup,
    tx,
    [userKeypair],
    {
      skipPreflight: true,
      commitment: "confirmed",
    }
  );
  
  const duration = Date.now() - start;
  console.log(`${duration}ms (ER) Increment txHash: ${txHash}`);
});

Bolt transactions

From the bolt-counter test:
import { ApplySystem } from "@magicblock-labs/bolt-sdk";

it("Apply the increase system", async () => {
  const applySystem = await ApplySystem({
    authority: providerEphemeralRollup.wallet.publicKey,
    world: worldPda,
    entities: [
      {
        entity: entityPda,
        components: [{ componentId: counterComponent.programId }],
      },
    ],
    systemId: systemIncrease.programId,
  });
  
  const tx = applySystem.transaction;
  tx.feePayer = provider.wallet.publicKey;
  tx.recentBlockhash = (
    await providerEphemeralRollup.connection.getLatestBlockhash()
  ).blockhash;
  
  const txSign = await providerEphemeralRollup.sendAndConfirm(tx, [], {
    skipPreflight: true,
  });
  
  console.log(`Applied system: ${txSign}`);
});

Transaction confirmation

ER transactions can be confirmed using standard Solana confirmation strategies:
// Option 1: sendAndConfirm (recommended)
const txHash = await providerEphemeralRollup.sendAndConfirm(tx, [], {
  skipPreflight: true,
  commitment: "confirmed",
});

// Option 2: Manual confirmation
const signature = await providerEphemeralRollup.connection.sendRawTransaction(
  tx.serialize()
);

await providerEphemeralRollup.connection.confirmTransaction(
  signature,
  "confirmed"
);
Use skipPreflight: true for faster transaction submission. Preflight checks add unnecessary latency in ERs.

Committing state

State changes in ERs can be committed to the base layer in three ways:

Automatic commits

State automatically commits at the interval specified during delegation:
let delegate_config = DelegateConfig {
    commit_frequency_ms: 30_000, // Commit every 30 seconds
    validator: None,
};

Manual commits

You can manually commit state from within your program:
/home/daytona/workspace/source/anchor-counter/programs/anchor-counter/src/lib.rs
use ephemeral_rollups_sdk::ephem::commit_accounts;

pub fn commit(ctx: Context<IncrementAndCommit>) -> Result<()> {
    commit_accounts(
        &ctx.accounts.payer,
        vec![&ctx.accounts.counter.to_account_info()],
        &ctx.accounts.magic_context,
        &ctx.accounts.magic_program,
    )?;
    Ok(())
}
Execute the commit from the client:
let tx = await program.methods
  .commit()
  .accounts({ payer: providerEphemeralRollup.wallet.publicKey })
  .transaction();

tx.feePayer = providerEphemeralRollup.wallet.publicKey;
tx.recentBlockhash = (
  await providerEphemeralRollup.connection.getLatestBlockhash()
).blockhash;

const txHash = await providerEphemeralRollup.sendAndConfirm(tx);

Tracking commit confirmations

Use the SDK to wait for commit finalization on the base layer:
import { GetCommitmentSignature } from "@magicblock-labs/ephemeral-rollups-sdk";

// Execute commit on ER
const txHash = await providerEphemeralRollup.sendAndConfirm(commitTx);
console.log("ER commit tx:", txHash);

// Wait for base layer confirmation
const baseTxHash = await GetCommitmentSignature(
  txHash,
  providerEphemeralRollup.connection
);
console.log("Base layer commit tx:", baseTxHash);
The commit transaction executes on the ER instantly, but takes several seconds to finalize on the base layer.

Commit and undelegate

To commit final state and return the account to the base layer:
/home/daytona/workspace/source/anchor-counter/programs/anchor-counter/src/lib.rs
use ephemeral_rollups_sdk::ephem::commit_and_undelegate_accounts;

pub fn increment_and_undelegate(ctx: Context<IncrementAndCommit>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.count += 1;
    
    // Serialize the account state
    counter.exit(&crate::ID)?;
    
    // Commit and undelegate in one operation
    commit_and_undelegate_accounts(
        &ctx.accounts.payer,
        vec![&ctx.accounts.counter.to_account_info()],
        &ctx.accounts.magic_context,
        &ctx.accounts.magic_program,
    )?;
    
    Ok(())
}
From the client:
let tx = await program.methods
  .incrementAndUndelegate()
  .accounts({ payer: providerEphemeralRollup.wallet.publicKey })
  .transaction();

tx.feePayer = providerEphemeralRollup.wallet.publicKey;
tx.recentBlockhash = (
  await providerEphemeralRollup.connection.getLatestBlockhash()
).blockhash;

const txHash = await providerEphemeralRollup.sendAndConfirm(tx);
console.log("Undelegate tx:", txHash);

// Wait for confirmation on base layer
const baseTxHash = await GetCommitmentSignature(
  txHash,
  providerEphemeralRollup.connection
);
console.log("Base layer undelegate tx:", baseTxHash);

Performance comparison

Typical transaction latencies:
OperationBase Layer (Solana)Ephemeral Rollup
Initialize2000-3000msN/A (done on base)
Delegate2000-3000msN/A (done on base)
Increment2000-3000ms50-100ms
CommitN/A50-100ms (ER) + 2000-3000ms (base confirmation)
Undelegate2000-3000ms (on ER) + wait for base confirmationN/A
Delegation and undelegation must be executed on the base layer connection, not the ER connection.

Transaction lifecycle example

Complete flow from the anchor-counter test:
1
Initialize on base layer
2
// Uses base layer connection
const tx = await program.methods.initialize()
  .accounts({ user: provider.wallet.publicKey })
  .transaction();
  
const txHash = await provider.sendAndConfirm(tx);
console.log("2847ms (Base Layer) Initialize");
3
Delegate to ER
4
// Uses base layer connection
const tx = await program.methods.delegate()
  .accounts({ payer: provider.wallet.publicKey, pda: counterPDA })
  .transaction();
  
const txHash = await provider.sendAndConfirm(tx);
console.log("2341ms (Base Layer) Delegate");

// Wait for delegation to propagate
await new Promise((resolve) => setTimeout(resolve, 3000));
5
Execute on ER (fast)
6
// Uses ER connection
let tx = await program.methods.increment()
  .accounts({ counter: counterPDA })
  .transaction();
  
tx.feePayer = providerER.wallet.publicKey;
tx.recentBlockhash = (await providerER.connection.getLatestBlockhash()).blockhash;
tx = await providerER.wallet.signTransaction(tx);

const txHash = await providerER.sendAndConfirm(tx);
console.log("73ms (ER) Increment"); // 30x faster!
7
Commit state
8
// Uses ER connection
let tx = await program.methods.commit()
  .accounts({ payer: providerER.wallet.publicKey })
  .transaction();
  
tx.feePayer = providerER.wallet.publicKey;
tx.recentBlockhash = (await providerER.connection.getLatestBlockhash()).blockhash;

const txHash = await providerER.sendAndConfirm(tx);
console.log("68ms (ER) Commit");

// Wait for base layer confirmation
const baseTxHash = await GetCommitmentSignature(txHash, providerER.connection);
console.log("2456ms (Base Layer) Commit finalized");
9
Undelegate
10
// Uses ER connection for transaction, but commits to base layer
let tx = await program.methods.incrementAndUndelegate()
  .accounts({ payer: providerER.wallet.publicKey })
  .transaction();
  
tx.feePayer = providerER.wallet.publicKey;
tx.recentBlockhash = (await providerER.connection.getLatestBlockhash()).blockhash;

const txHash = await providerER.sendAndConfirm(tx);
console.log("81ms (ER) Undelegate");

Best practices

Skip preflight

Use skipPreflight: true to reduce latency. ER validators handle validation efficiently.

Batch operations

Execute multiple operations on the ER before committing to minimize base layer costs.

Connection management

Maintain separate connections for base layer and ER operations.

Error handling

Handle ER connection failures gracefully and retry on the base layer if needed.

Examples

Next steps

Account delegation

Learn how to delegate accounts to ERs

Ephemeral Rollups

Understand the complete ER architecture

Build docs developers (and LLMs) love