The instruction update_leaderboard runs on the base layer immediately after the commit lands. The #[action] attribute on its accounts context marks it as callable from a post-commit action.
// program instructionpub 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(())}// instruction context#[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>,}
The commit instruction commit_and_update_leaderboard runs on the ER. It uses MagicIntentBundleBuilder to schedule both the commit and the post-commit action onto magic_context — both are applied together when the ER transaction is sealed back to the base layer.
// commit action instruction on ERpub fn commit_and_update_leaderboard(ctx: Context<CommitAndUpdateLeaderboard>) -> Result<()> { // Build the post-commit action that updates the leaderboard on base layer 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, // Signer that pays transaction fees for the action from its escrow PDA escrow_authority: ctx.accounts.payer.to_account_info(), compute_units: 200_000, }; // Schedule commit + post-commit action on magic_context MagicIntentBundleBuilder::new( ctx.accounts.payer.to_account_info(), ctx.accounts.magic_context.to_account_info(), ctx.accounts.magic_program.to_account_info(), ) .commit(&[ctx.accounts.counter.to_account_info()]) .add_post_commit_actions([action]) .build_and_invoke()?; Ok(())}// commit action context on ER#[commit]#[derive(Accounts)]pub struct CommitAndUpdateLeaderboard<'info> { #[account(mut)] pub payer: Signer<'info>, #[account(mut, seeds = [COUNTER_SEED], bump)] pub counter: Account<'info, Counter>, /// CHECK: Leaderboard PDA - not mut here, writable set in handler #[account(seeds = [LEADERBOARD_SEED], bump)] pub leaderboard: UncheckedAccount<'info>, /// CHECK: Your program ID pub program_id: AccountInfo<'info>,}
You can commit multiple accounts and chain several actions in one call. Actions execute sequentially in the order they’re passed to add_post_commit_actions.
// Chain several actions — they execute sequentially on base layer after the commit lands.MagicIntentBundleBuilder::new( ctx.accounts.payer.to_account_info(), ctx.accounts.magic_context.to_account_info(), ctx.accounts.magic_program.to_account_info(),).commit(&[ ctx.accounts.counter.to_account_info(), // ... additional committed accounts]).add_post_commit_actions([action_1, action_2, action_3]).build_and_invoke()?;
Actions can also be chained onto an undelegation — the counter commits, undelegates, and the actions run, all atomically in one ER transaction.
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.MagicIntentBundleBuilder::new( ctx.accounts.payer.to_account_info(), ctx.accounts.magic_context.to_account_info(), ctx.accounts.magic_program.to_account_info(),).commit_and_undelegate(&[ctx.accounts.counter.to_account_info()]).add_post_commit_actions([action]).build_and_invoke()?;