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
/// 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(())}
// 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()?;
Action 也可以串联到解除委托上 —— counter 提交、解除委托以及 action 全部在同一个 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 —— 重复使用会与现有 PDA 冲突。
提交到 base-layer RPC,而不是 ER。
目标账户必须已被委托。
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 };}