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.

A simple counter program demonstrating how to use the Anchor framework with Ephemeral Rollups. This example shows how to delegate accounts, execute low-latency transactions, and commit state back to the base layer using Anchor’s ergonomic macros.

What You’ll Learn

  • How to use Anchor’s #[ephemeral], #[delegate], and #[commit] macros
  • How to delegate a PDA to Ephemeral Rollups
  • How to execute transactions with low latency on delegated accounts
  • How to commit state changes back to Solana
  • How to undelegate accounts from Ephemeral Rollups

Program Structure

The Anchor counter program includes the following instructions:
  • initialize - Initialize the counter PDA to 0
  • increment - Increment the counter by 1 (with rollover at 1000)
  • delegate - Delegate the counter account to the delegation program
  • commit - Manually commit the account state to Solana
  • undelegate - Commit and undelegate the account
  • increment_and_commit - Increment and commit in one transaction
  • increment_and_undelegate - Increment and undelegate in one transaction

Software Requirements

Ensure you have the following software packages installed before building the program.
SoftwareVersionInstallation Guide
Solana2.3.13Install Solana
Rust1.85.0Install Rust
Anchor0.32.1Install Anchor
Node24.10.0Install Node
# Check and initialize your Solana version
agave-install list
agave-install init 2.3.13

# Check and initialize your Rust version
rustup show
rustup install 1.85.0

# Check and initialize your Anchor version
avm list
avm use 0.32.1

Build and Test

1

Install dependencies

yarn
2

Run tests (skip build and deploy)

The test script automatically detects the cluster from Anchor.toml and handles Ephemeral Rollup setup for localnet:
anchor test --skip-deploy --skip-build --skip-local-validator
3

Build, deploy and test (optional)

To build, deploy and run tests with a new program:
# Delete keypairs in the deploy folder
rm -rf /target/deploy/*.keypair

# Build, deploy and test program
anchor test

Program Implementation

Delegation Macro

The program uses Anchor’s special macros to enable Ephemeral Rollups integration:
use anchor_lang::prelude::*;
use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral};
use ephemeral_rollups_sdk::cpi::DelegateConfig;

#[ephemeral]
#[program]
pub mod anchor_counter {
    use super::*;
    // ... instructions
}
The #[ephemeral] macro marks the program as compatible with Ephemeral Rollups, enabling special delegation features.

Delegate Instruction

The delegate instruction uses the #[delegate] macro on the context struct:
/// Delegate the account to the delegation program
pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> {
    ctx.accounts.delegate_pda(
        &ctx.accounts.payer,
        &[COUNTER_SEED],
        DelegateConfig {
            // Optionally set a specific validator from the first remaining account
            validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
            ..Default::default()
        },
    )?;
    Ok()
}

/// Add delegate function to the context
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
    pub payer: Signer<'info>,
    /// CHECK The pda to delegate
    #[account(mut, del)]
    pub pda: AccountInfo<'info>,
}
The #[delegate] macro automatically adds the required delegation accounts to the context, and the #[account(mut, del)] attribute marks which account to delegate.

Increment Instruction

The core increment logic is simple:
/// Increment the counter.
pub fn increment(ctx: Context<Increment>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.count += 1;
    if counter.count > 1000 {
        counter.count = 0;
    }
    msg!("PDA {} count: {}", counter.key(), counter.count);
    Ok()
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut, seeds = [COUNTER_SEED], bump)]
    pub counter: Account<'info, Counter>,
}

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

Commit and Undelegate

The #[commit] macro simplifies committing state changes:
/// Manual commit the account in the ER.
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()
}

/// Undelegate the account from the delegation program
pub fn undelegate(ctx: Context<IncrementAndCommit>) -> Result<()> {
    commit_and_undelegate_accounts(
        &ctx.accounts.payer,
        vec![&ctx.accounts.counter.to_account_info()],
        &ctx.accounts.magic_context,
        &ctx.accounts.magic_program,
    )?;
    Ok()
}

/// Account for the increment instruction + manual commit.
#[commit]
#[derive(Accounts)]
pub struct IncrementAndCommit<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, seeds = [COUNTER_SEED], bump)]
    pub counter: Account<'info, Counter>,
}
The #[commit] macro automatically adds the magic_program and magic_context accounts required for committing state.

TypeScript Client Usage

Initialize and Increment on Solana

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { AnchorCounter } from "../target/types/anchor_counter";

const provider = anchor.AnchorProvider.env();
const program = anchor.workspace.AnchorCounter as Program<AnchorCounter>;

const COUNTER_SEED = "counter";
const [counterPDA] = anchor.web3.PublicKey.findProgramAddressSync(
  [Buffer.from(COUNTER_SEED)],
  program.programId
);

// Initialize counter
let tx = await program.methods
  .initialize()
  .accounts({
    user: provider.wallet.publicKey,
  })
  .transaction();

await provider.sendAndConfirm(tx, [provider.wallet.payer]);

// Increment on Solana
tx = await program.methods
  .increment()
  .accounts({
    counter: counterPDA,
  })
  .transaction();

await provider.sendAndConfirm(tx, [provider.wallet.payer]);

Delegate to Ephemeral Rollups

// Add local validator identity if running on localnet
const remainingAccounts = providerEphemeralRollup.connection.rpcEndpoint.includes("localhost")
  ? [{
      pubkey: new web3.PublicKey("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"),
      isSigner: false,
      isWritable: false,
    }]
  : [];

let tx = await program.methods
  .delegate()
  .accounts({
    payer: provider.wallet.publicKey,
    pda: counterPDA,
  })
  .remainingAccounts(remainingAccounts)
  .transaction();

await provider.sendAndConfirm(tx, [provider.wallet.payer]);

Execute on Ephemeral Rollups

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()
);

let tx = await program.methods
  .increment()
  .accounts({
    counter: counterPDA,
  })
  .transaction();

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

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

Key Features

Ergonomic Macros

Use #[ephemeral], #[delegate], and #[commit] macros to simplify Ephemeral Rollups integration

Automatic Account Injection

Delegation and commit accounts are automatically added to instruction contexts

Type Safety

Anchor’s type-safe framework ensures correct account structures and validation

Flexible Commits

Commit state manually or combine with other operations in a single instruction

Source Code

View the complete source code on GitHub: anchor-counter on GitHub

Build docs developers (and LLMs) love