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.

Token Transfer

The Token Transfer example demonstrates how to work with custom token accounts in Ephemeral Rollups. While this example uses a simple balance account for clarity, the same patterns apply to SPL tokens, allowing you to build high-frequency token operations with ultra-low latency.

Overview

This example shows:
  • Creating and delegating balance accounts
  • Transferring tokens between accounts in ERs
  • Configurable delegation parameters
  • Committing and undelegating accounts
While this example uses a simple Balance account, the same delegation patterns work with SPL Token Accounts for real token transfers.

How It Works

1
Initialize Balance Accounts
2
Each user has a balance account (PDA) seeded by their public key:
3
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let acc = &mut ctx.accounts.balance;
    acc.balance = 100;
    Ok()
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8, seeds = [user.key.as_ref()], bump)]
    pub balance: Account<'info, Balance>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct Balance {
    pub balance: u64,
}
4
Delegate with Custom Parameters
5
The delegation includes configurable parameters for commit frequency and validator selection:
6
pub fn delegate(ctx: Context<DelegateBalance>, params: DelegateParams) -> Result<()> {
    let config = DelegateConfig {
        commit_frequency_ms: params.commit_frequency_ms,
        validator: params.validator,
    };

    ctx.accounts.delegate_balance(
        &ctx.accounts.payer,
        &[ctx.accounts.payer.key.as_ref()],
        config,
    )?;
    Ok()
}

#[delegate]
#[derive(Accounts)]
pub struct DelegateBalance<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, del, seeds = [payer.key.as_ref()], bump)]
    pub balance: AccountInfo<'info>,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct DelegateParams {
    pub commit_frequency_ms: u32,
    pub validator: Option<Pubkey>,
}
7
Transfer Between Accounts
8
Once delegated, transfers execute with ultra-low latency in the ER:
9
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
    let balance = &mut ctx.accounts.balance;
    let receiver_balance = &mut ctx.accounts.receiver_balance;
    if balance.balance < amount {
        return Err(error!(ErrorCode::InsufficientBalance));
    }
    balance.balance -= amount;
    receiver_balance.balance += amount;
    Ok()
}

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, seeds = [payer.key.as_ref()], bump)]
    pub balance: Account<'info, Balance>,
    /// CHECK: anyone can receive the tokens
    pub receiver: AccountInfo<'info>,
    #[account(init_if_needed, payer = payer, space = 8 + 8, seeds = [receiver.key.as_ref()], bump)]
    pub receiver_balance: Account<'info, Balance>,
    pub system_program: Program<'info, System>,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Insufficient balance for transfer")]
    InsufficientBalance,
}
10
Undelegate and Commit
11
When you’re done with high-frequency operations, undelegate to commit final state:
12
pub fn undelegate(ctx: Context<UndelegateBalance>) -> Result<()> {
    commit_and_undelegate_accounts(
        &ctx.accounts.payer,
        vec![&ctx.accounts.balance.to_account_info()],
        &ctx.accounts.magic_context,
        &ctx.accounts.magic_program,
    )?;
    Ok()
}

#[commit]
#[derive(Accounts)]
pub struct UndelegateBalance<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut, seeds = [payer.key.as_ref()], bump)]
    pub balance: Account<'info, Balance>,
}

Program Annotations

The program uses the #[ephemeral] macro to enable ER support:
programs/dummy-transfer/src/lib.rs
use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral};
use ephemeral_rollups_sdk::cpi::DelegateConfig;
use ephemeral_rollups_sdk::ephem::commit_and_undelegate_accounts;

#[ephemeral]
#[program]
pub mod dummy_transfer {
    use super::*;
    // ... program code
}

Delegation Configuration

DelegateConfig {
    commit_frequency_ms: 30000, // Commit every 30 seconds
    validator: None,
}
Set commit_frequency_ms based on your use case:
  • Gaming: 10-30 seconds for responsive state updates
  • Trading: 5-10 seconds for more frequent commits
  • High-value operations: Lower values for more frequent base layer syncs

Complete Flow Example

import { Program } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";

// 1. Initialize balance account on base layer
const initTx = await program.methods
  .initialize()
  .accounts({
    user: wallet.publicKey,
  })
  .rpc();

// 2. Delegate to ER with custom config
const delegateTx = await program.methods
  .delegate({
    commitFrequencyMs: 30000,
    validator: null,
  })
  .accounts({
    payer: wallet.publicKey,
  })
  .rpc();

// 3. Perform rapid transfers in ER
for (let i = 0; i < 100; i++) {
  await program.methods
    .transfer(new BN(1))
    .accounts({
      payer: wallet.publicKey,
      receiver: recipientPublicKey,
    })
    .rpc(); // Ultra-low latency in ER!
}

// 4. Undelegate and commit final state
const undelegateTx = await program.methods
  .undelegate()
  .accounts({
    payer: wallet.publicKey,
  })
  .rpc();

SPL Token Integration

To use this pattern with real SPL tokens:
1
Add SPL Token Dependencies
2
[dependencies]
anchor-spl = "0.32.1"
spl-token = "6.0.0"
3
Update Account Structure
4
Replace the Balance account with SPL Token Account:
5
use anchor_spl::token::{Token, TokenAccount};

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        mut,
        associated_token::mint = mint,
        associated_token::authority = authority
    )]
    pub from: Account<'info, TokenAccount>,
    #[account(
        mut,
        associated_token::mint = mint,
        associated_token::authority = to_authority
    )]
    pub to: Account<'info, TokenAccount>,
    pub mint: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
}
6
Delegate Token Accounts
7
pub fn delegate_token_account(ctx: Context<DelegateTokenAccount>) -> Result<()> {
    ctx.accounts.delegate_pda(
        &ctx.accounts.payer,
        &[], // Token accounts don't use seeds
        DelegateConfig::default(),
    )?;
    Ok()
}

What Makes This Advanced?

This example demonstrates:
  1. Custom Delegation Parameters: Fine-grained control over commit frequency and validator selection
  2. Multi-Account Operations: Transferring between multiple delegated accounts
  3. Init-If-Needed Pattern: Automatically creating receiver accounts during transfers
  4. Error Handling: Proper balance validation with custom errors
  5. PDA Management: Using PDAs for user-specific balance accounts

Use Cases

  • In-game currency transfers
  • Rapid item trading between players
  • Reward distributions
  • Marketplace transactions

Performance Benefits

Compared to base layer token transfers:
  • Latency: ~400ms → ~10ms (40x faster)
  • Cost: ~0.000005 SOL → negligible in ER
  • Throughput: Thousands of transfers per second
  • Batching: Execute 100s of transfers before committing

Testing Locally

1
Install the Local Validator
2
npm install -g @magicblock-labs/ephemeral-validator
3
Start the Local Validator
4
ACCOUNTS_REMOTE=https://rpc.magicblock.app/devnet ACCOUNTS_LIFECYCLE=ephemeral ephemeral-validator
5
Run Tests
6
PROVIDER_ENDPOINT=http://localhost:8899 WS_ENDPOINT=ws://localhost:8900 anchor test --skip-build --skip-deploy --skip-local-validator

Testing on Devnet

To run tests on devnet:
anchor test --skip-local-validator --skip-build --skip-deploy
Make sure you have devnet SOL in your wallet before running devnet tests.

Security Considerations

Important:
  • Always validate account ownership before transfers
  • Check sufficient balance before debiting
  • Use proper PDA derivation for user accounts
  • Set appropriate commit frequencies for your use case
  • Monitor delegated account states

Adapting for Real Tokens

The key differences when using SPL tokens:
  1. Account Type: TokenAccount instead of custom Balance
  2. Authority: Token account authority, not PDA seeds
  3. Transfer Logic: Use SPL token CPI instead of direct balance updates
  4. Mint Validation: Ensure all accounts use the same mint
Example SPL transfer in ER:
use anchor_spl::token;

pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            token::Transfer {
                from: ctx.accounts.from.to_account_info(),
                to: ctx.accounts.to.to_account_info(),
                authority: ctx.accounts.authority.to_account_info(),
            },
        ),
        amount,
    )?;
    Ok()
}
This transfer executes in the ER with the same ultra-low latency as the balance example!

Next Steps

Build docs developers (and LLMs) love