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.

Magic Actions

Magic Actions enable you to execute automatic on-chain handlers when committing accounts from Ephemeral Rollups back to the base layer. This powerful pattern allows you to perform base chain operations (like updating global state) automatically whenever ER state is committed.

What Are Magic Actions?

Magic Actions are instruction handlers that execute automatically on the base chain when you commit delegated accounts from an Ephemeral Rollup. They enable:
  • Updating global leaderboards when player scores are committed
  • Triggering reward distributions based on ER activity
  • Synchronizing cross-program state
  • Executing base-layer-only operations (like SPL token transfers) on commit
Magic Actions execute on the base layer (Solana mainnet/devnet) using funds from an escrow account, allowing automated base chain interactions without user signatures.

How It Works

1
Mark Your Handler with #[action]
2
Create an instruction that will execute on the base layer when accounts are committed:
3
#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
    #[account(mut, seeds = [LEADERBOARD_SEED], bump)]
    pub leaderboard: Account<'info, Leaderboard>,
    /// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID
    pub counter: UncheckedAccount<'info>,
}

pub fn update_leaderboard(ctx: Context<UpdateLeaderboard>) -> Result<()> {
    let leaderboard = &mut ctx.accounts.leaderboard;
    let counter_info = &mut ctx.accounts.counter.to_account_info();
    let mut data: &[u8] = &counter_info.try_borrow_data()?;
    let counter = Counter::try_deserialize(&mut data)?;

    if counter.count > leaderboard.high_score {
        leaderboard.high_score = counter.count;
    }

    msg!(
        "Leaderboard updated! High score: {}",
        leaderboard.high_score
    );
    Ok()
}
4
Build the Magic Action Instruction
5
Create the action instruction that will execute on commit:
6
pub fn commit_and_update_leaderboard(ctx: Context<CommitAndUpdateLeaderboard>) -> Result<()> {
    // Create action instruction
    let instruction_data =
        anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {});
    let action_args = ActionArgs::new(instruction_data);
    let action_accounts = vec![
        ShortAccountMeta {
            pubkey: ctx.accounts.leaderboard.key(),
            is_writable: true,
        },
        ShortAccountMeta {
            pubkey: ctx.accounts.counter.key(),
            is_writable: false,
        },
    ];
    let action = CallHandler {
        destination_program: crate::ID,
        accounts: action_accounts,
        args: action_args,
        escrow_authority: ctx.accounts.payer.to_account_info(), // Signer authorized to pay transaction fees for action from escrow PDA
        compute_units: 200_000,
    };

    // Build commit and action instruction
    let magic_action = MagicInstructionBuilder {
        payer: ctx.accounts.payer.to_account_info(),
        magic_context: ctx.accounts.magic_context.to_account_info(),
        magic_program: ctx.accounts.magic_program.to_account_info(),
        magic_action: MagicAction::Commit(CommitType::WithHandler {
            commited_accounts: vec![ctx.accounts.counter.to_account_info()],
            call_handlers: vec![action],
        }),
    };

    // Invoke
    magic_action.build_and_invoke()?;

    Ok()
}
7
Set Up Escrow Account
8
Magic Actions require an escrow account to pay for base layer transaction fees:
9
import {
  createTopUpEscrowInstruction,
  createCloseEscrowInstruction,
  escrowPdaFromEscrowAuthority,
} from "@magicblock-labs/ephemeral-rollups-sdk";

// Create and fund the escrow
const topUpEscrowIx = createTopUpEscrowInstruction(
  escrowPdaFromEscrowAuthority(anchor.Wallet.local().publicKey),
  anchor.Wallet.local().publicKey,
  anchor.Wallet.local().publicKey,
  10000 // top-up amount in lamports
);

// Combine with delegation
const delegateIx = await program.methods
  .delegate()
  .accounts({
    payer: anchor.Wallet.local().publicKey,
    pda: pda
  })
  .remainingAccounts(remainingAccounts)
  .instruction();

const tx = new Transaction().add(topUpEscrowIx, delegateIx);
const signature = await sendAndConfirmTransaction(
  routerConnection,
  tx,
  [anchor.Wallet.local().payer],
  { skipPreflight: true }
);
10
Trigger the Action
11
Call the commit instruction with the attached action handler:
12
const tx = await program.methods
  .commitAndUpdateLeaderboard()
  .accounts({
    payer: anchor.Wallet.local().publicKey,
    programId: program.programId,
  })
  .transaction();

const signature = await sendAndConfirmTransaction(
  routerConnection,
  tx,
  [anchor.Wallet.local().payer],
  { skipPreflight: true }
);

// The leaderboard will be updated on the base layer automatically

Required Imports

use ephemeral_rollups_sdk::anchor::{action, commit, delegate, ephemeral};
use ephemeral_rollups_sdk::ephem::{CallHandler, CommitType, MagicAction, MagicInstructionBuilder};
use ephemeral_rollups_sdk::{ActionArgs, ShortAccountMeta};

Account Context Annotations

#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
    #[account(mut, seeds = [LEADERBOARD_SEED], bump)]
    pub leaderboard: Account<'info, Leaderboard>,
    /// CHECK: Account may be delegated or undelegated
    pub counter: UncheckedAccount<'info>,
}

Managing Escrow Accounts

import { createTopUpEscrowInstruction, escrowPdaFromEscrowAuthority } from "@magicblock-labs/ephemeral-rollups-sdk";

const escrowPda = escrowPdaFromEscrowAuthority(wallet.publicKey);
const topUpIx = createTopUpEscrowInstruction(
  escrowPda,
  wallet.publicKey,
  wallet.publicKey,
  10000 // lamports to fund the escrow
);
Ensure your escrow account has sufficient funds to cover the transaction fees for all Magic Actions you plan to execute. Each action consumes base layer transaction fees from the escrow.

What Makes This Advanced?

Magic Actions represent an advanced pattern because they:
  1. Bridge ER and Base Layer: Automatically execute base chain logic when ER state is committed
  2. Gasless Automation: Use escrow accounts to pay for base layer fees without user interaction
  3. Cross-Program Coordination: Update global state (like leaderboards) based on individual player actions in ERs
  4. Composability: Chain multiple actions together in a single commit operation

Use Cases

  • Global Leaderboards: Update rankings when player scores are committed from game ERs
  • Tournament Systems: Trigger prize distributions when tournament state is finalized
  • Cross-Chain State: Synchronize ER state with base layer contracts
  • Token Operations: Execute SPL token transfers on the base layer when ER conditions are met

Example: Counter with Leaderboard

programs/magic-actions/src/lib.rs
pub const COUNTER_SEED: &[u8] = b"counter";
pub const LEADERBOARD_SEED: &[u8] = b"leaderboard";

#[account]
pub struct Counter {
    pub count: u64,
}

#[account]
pub struct Leaderboard {
    pub high_score: u64,
}
The counter is delegated and runs in an ER with ultra-low latency. The leaderboard stays on the base layer for global visibility. When the counter is committed, the Magic Action automatically checks if the score beats the high score and updates the leaderboard.

Next Steps

Build docs developers (and LLMs) love