Documentation Index
Fetch the complete documentation index at: https://docs.magicblock.gg/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This guide demonstrates how to implement cranks using MagicBlock’s Ephemeral Rollups (ER) with the Anchor framework. The implementation follows this flow:
- Initialize counter on Solana base layer
- Delegate counter account to Ephemeral Rollup for faster execution
- Schedule a crank task that automatically increments the counter
- Execute the crank automatically at specified intervals
- Undelegate the account back to Solana base layer when done
Execution Flow
1. User calls initialize() on Solana base layer
└─> Creates Counter PDA with count = 0
2. User calls delegate() on Solana base layer
└─> Moves Counter account to Ephemeral Rollup
3. User calls schedule_increment() on Ephemeral Rollup
└─> CPI to MagicBlock program
└─> Schedules task with:
- task_id: 1
- interval: 100ms
- iterations: 3
- instruction: increment()
4. MagicBlock automatically executes increment() 3 times:
└─> Execution 1: count = 1 (at T+0ms)
└─> Execution 2: count = 2 (at T+100ms)
└─> Execution 3: count = 3 (at T+200ms)
5. User calls undelegate() on Ephemeral Rollup
└─> Commits changes and moves Counter back to Solana base layer
Core Components
Scheduled Function
Let’s take for example that you wanted to schedule the following simple increment instrcution for a 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(())
}
Schedule Increment Function
The core crank scheduling logic:
pub fn schedule_increment(ctx: Context<ScheduleIncrement>, args: ScheduleIncrementArgs) -> Result<()> {
let increment_ix = Instruction {
program_id: crate::ID,
accounts: vec![AccountMeta::new(ctx.accounts.counter.key(), false)],
// Defining the instruction to call.
data: anchor_lang::InstructionData::data(&crate::instruction::Increment {}),
};
let ix_data = bincode::serialize(&MagicBlockInstruction::ScheduleTask(
ScheduleTaskArgs {
task_id: args.task_id,
execution_interval_millis: args.execution_interval_millis,
iterations: args.iterations,
instructions: vec![increment_ix],
},
))
.map_err(|err| {
msg!("ERROR: failed to serialize args {:?}", err);
ProgramError::InvalidArgument
})?;
let schedule_ix = Instruction::new_with_bytes(
MAGIC_PROGRAM_ID,
&ix_data,
vec![
AccountMeta::new(ctx.accounts.payer.key(), true),
AccountMeta::new(ctx.accounts.counter.key(), false),
],
);
invoke_signed(
&schedule_ix,
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.counter.to_account_info(),
],
&[],
)?;
Ok(())
}
Key Points:
- Creates an instruction to increment the counter
- Serializes it into a
ScheduleTask instruction for MagicBlock
- Uses CPI (Cross-Program Invocation) to call the MagicBlock program
- The MagicBlock program handles the scheduling and execution
Schedule Arguments
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ScheduleIncrementArgs {
pub task_id: u64, // Unique identifier for the task
pub execution_interval_millis: u64, // Time between executions in milliseconds
pub iterations: u64, // Number of times to execute
}
ScheduleIncrement Context
#[derive(Accounts)]
pub struct ScheduleIncrement<'info> {
/// CHECK: used for CPI
#[account()]
pub magic_program: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: Passed to CPI - using AccountInfo to avoid Anchor re-serializing stale data after CPI
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: AccountInfo<'info>,
/// CHECK: used for CPI
pub program: AccountInfo<'info>,
}
Important: Uses AccountInfo instead of Account<Counter> to avoid Anchor re-serializing stale data after CPI calls.