The undelegation callback discriminator [196, 28, 41, 206, 48, 37, 51, 167]
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.With [#ephemeral] Anchor macro from MagicBlock’s Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
#[ephemeral]#[program]pub mod public_counter { use super::*; /// Initialize the counter. pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = 0; Ok(()) } /// Increment the counter. pub fn increment(ctx: Context<Increment>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; Ok(()) } /// Delegate the account to the delegation program /// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> { // ... } /// Manually commit the counter state in the Ephemeral Rollup session. pub fn commit(ctx: Context<IncrementAndCommit>) -> Result<()> { // ... } /// Increment the counter and commit in the same instruction. pub fn increment_and_commit(ctx: Context<IncrementAndCommit>) -> Result<()> { // ... } /// Undelegate the account from the delegation program. pub fn undelegate(ctx: Context<IncrementAndCommit>) -> Result<()> { // ... }}pub const COUNTER_SEED: &[u8] = b"counter";/// Context for initializing counter#[derive(Accounts)]pub struct Initialize<'info> { #[account(init_if_needed, payer = user, space = 8 + 8, seeds = [COUNTER_SEED], bump)] pub counter: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>,}/// Context for incrementing counter#[derive(Accounts)]pub struct Increment<'info> { #[account(mut, seeds = [COUNTER_SEED], bump)] pub counter: Account<'info, Counter>,}/// Counter struct#[account]pub struct Counter { pub count: u64,}/// Other context for delegation
特別なことはなく、単純な Anchor カウンタープログラムです。違いは、undelegation 用の ephemeral macro と、delegation program とやり取りするロジックを注入する delegate macro を追加していることです。⬆️ Back to Top
これらの公開バリデータは開発用として利用できます。委任命令には、
対象となる ER バリデータを必ず追加してください。
/// 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>,}
/// Delegate the account to the delegation program/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setuppub 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(())}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;/// Manually commit the counter state in the Ephemeral Rollup session.pub fn commit(ctx: Context<IncrementAndCommit>) -> Result<()> { 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()]) .build_and_invoke()?; Ok(())}/// Increment the counter and commit the new state in the same instruction.pub fn increment_and_commit(ctx: Context<IncrementAndCommit>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; // Serialize the Anchor account before the CPI sees it counter.exit(&crate::ID)?; 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()]) .build_and_invoke()?; Ok(())}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;/// Undelegate the account from the delegation program./// Commits the latest state and returns ownership of the PDA back to the owner program.pub fn undelegate(ctx: Context<IncrementAndCommit>) -> Result<()> { 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()]) .build_and_invoke()?; Ok(())}
commit_and_update_leaderboard コミット命令は ER 上で実行されます。MagicIntentBundleBuilder を使用してコミットとコミット後アクションの両方を magic_context に予約します — ER トランザクションがベースレイヤーへ確定されるとき、両者は同時に適用されます。
// 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>,}
// 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()?;
アクションは委任解除にも連鎖できます — counter のコミット、委任解除、アクションすべてが同一の ER トランザクション内でアトミックに実行されます。
// 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()?;
チャージごとに crypto.getRandomValues で新しい 32 バイトの salt を生成します — salt を使い回すと既存の PDA と衝突します。
ER ではなく base layer の RPC に提出します。
送金先アカウントはすでに委任済みである必要があります。
import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction,} from "@solana/web3.js";import { lamportsDelegatedTransferIx, deriveLamportsPda,} from "@magicblock-labs/ephemeral-rollups-sdk";/** * Top up a delegated account with lamports. * * The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token * program creates a single-use lamports PDA, funds it from the payer, and * delegates it so the ER credits the destination's delegated balance. */async function topUpDelegatedAccount( connection: Connection, // base-layer connection payer: Keypair, destination: PublicKey, // delegated account to top up amountLamports: bigint,) { // Generate a fresh 32-byte salt per top-up. // Re-using a salt collides with an existing lamports PDA and the call fails. const salt = crypto.getRandomValues(new Uint8Array(32)); const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt); const ix = await lamportsDelegatedTransferIx( payer.publicKey, destination, amountLamports, salt, ); const tx = new Transaction().add(ix); tx.feePayer = payer.publicKey; // CRITICAL: send to the base-layer RPC, not the ER. const sig = await sendAndConfirmTransaction(connection, tx, [payer], { commitment: "confirmed", skipPreflight: true, }); return { sig, lamportsPda };}