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.
Once accounts are delegated to an Ephemeral Rollup, you can execute transactions with minimal latency (typically 50-100ms). Transactions in ERs work identically to Solana transactions, but connect to the ER endpoint instead of the base layer.
Overview
Transactions in Ephemeral Rollups:
Execute on delegated accounts with sub-100ms latency
Use the same instruction format as Solana
Support all program types (Anchor, native Rust, Bolt, etc.)
Automatically commit to the base layer at configured intervals
Can be manually committed or undelegated at any time
Connection setup
To execute transactions on an ER, create a connection to the ER endpoint instead of the base layer.
Anchor connection
From the anchor-counter example:
/home/daytona/workspace/source/anchor-counter/tests/anchor-counter.ts
import * as anchor from "@coral-xyz/anchor" ;
// Base layer connection (Solana)
const provider = anchor . AnchorProvider . env ();
// Ephemeral Rollup connection
const providerEphemeralRollup = new anchor . AnchorProvider (
new anchor . web3 . Connection (
process . env . EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app/" ,
{
wsEndpoint:
process . env . EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/" ,
}
),
anchor . Wallet . local ()
);
console . log ( "Base Layer:" , provider . connection . rpcEndpoint );
console . log ( "Ephemeral Rollup:" , providerEphemeralRollup . connection . rpcEndpoint );
Web3.js connection
From the rust-counter example:
/home/daytona/workspace/source/rust-counter/tests/web3js/rust-counter.test.ts
import { Connection } from "@solana/web3.js" ;
// Base layer connection
const connectionBaseLayer = new Connection (
process . env . PROVIDER_ENDPOINT || "https://api.devnet.solana.com" ,
{ wsEndpoint: process . env . WS_ENDPOINT || "wss://api.devnet.solana.com" }
);
// Ephemeral Rollup connection
const connectionEphemeralRollup = new Connection (
process . env . EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app/" ,
{ wsEndpoint: process . env . EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/" }
);
You need both connections: one for base layer operations (delegation/undelegation) and one for ER execution.
Environment configuration
Set these environment variables to configure your ER endpoints:
export EPHEMERAL_PROVIDER_ENDPOINT = "https://devnet-as.magicblock.app/"
export EPHEMERAL_WS_ENDPOINT = "wss://devnet-as.magicblock.app/"
# Base layer (local Solana test validator)
export PROVIDER_ENDPOINT = "http://localhost:8899"
export WS_ENDPOINT = "ws://localhost:8900"
# Ephemeral Rollup (local MagicBlock validator)
export EPHEMERAL_PROVIDER_ENDPOINT = "http://localhost:7799"
export EPHEMERAL_WS_ENDPOINT = "ws://localhost:7800"
Executing transactions
Transactions on ERs follow the same pattern as Solana, but use the ER connection.
Anchor transactions
From the anchor-counter test:
it ( "Increase counter on ER" , async () => {
const start = Date . now ();
// Build the transaction
let tx = await program . methods
. increment ()
. accounts ({
counter: counterPDA ,
})
. transaction ();
// Set fee payer and recent blockhash from ER
tx . feePayer = providerEphemeralRollup . wallet . publicKey ;
tx . recentBlockhash = (
await providerEphemeralRollup . connection . getLatestBlockhash ()
). blockhash ;
// Sign with ER wallet
tx = await providerEphemeralRollup . wallet . signTransaction ( tx );
// Send to ER
const txHash = await providerEphemeralRollup . sendAndConfirm ( tx );
const duration = Date . now () - start ;
console . log ( ` ${ duration } ms (ER) Increment txHash: ${ txHash } ` );
// Typical output: "87ms (ER) Increment txHash: ..."
});
Native Web3.js transactions
From the rust-counter test:
import { Transaction , TransactionInstruction , sendAndConfirmTransaction } from "@solana/web3.js" ;
import * as borsh from "borsh" ;
it ( "Increase counter on ER" , async () => {
const start = Date . now ();
// Create transaction
const tx = new Transaction ();
// Define accounts
const keys = [
{
pubkey: userKeypair . publicKey ,
isSigner: true ,
isWritable: true ,
},
{
pubkey: counterPda ,
isSigner: false ,
isWritable: true ,
},
];
// Serialize instruction data
const serializedInstructionData = Buffer . concat ([
Buffer . from ( CounterInstruction . IncreaseCounter , "hex" ),
borsh . serialize ( IncreaseCounterPayload . schema , new IncreaseCounterPayload ( 1 )),
]);
// Create instruction
const incrementIx = new TransactionInstruction ({
keys: keys ,
programId: PROGRAM_ID ,
data: serializedInstructionData ,
});
tx . add ( incrementIx );
// Send to ER
const txHash = await sendAndConfirmTransaction (
connectionEphemeralRollup ,
tx ,
[ userKeypair ],
{
skipPreflight: true ,
commitment: "confirmed" ,
}
);
const duration = Date . now () - start ;
console . log ( ` ${ duration } ms (ER) Increment txHash: ${ txHash } ` );
});
Bolt transactions
From the bolt-counter test:
import { ApplySystem } from "@magicblock-labs/bolt-sdk" ;
it ( "Apply the increase system" , async () => {
const applySystem = await ApplySystem ({
authority: providerEphemeralRollup . wallet . publicKey ,
world: worldPda ,
entities: [
{
entity: entityPda ,
components: [{ componentId: counterComponent . programId }],
},
],
systemId: systemIncrease . programId ,
});
const tx = applySystem . transaction ;
tx . feePayer = provider . wallet . publicKey ;
tx . recentBlockhash = (
await providerEphemeralRollup . connection . getLatestBlockhash ()
). blockhash ;
const txSign = await providerEphemeralRollup . sendAndConfirm ( tx , [], {
skipPreflight: true ,
});
console . log ( `Applied system: ${ txSign } ` );
});
Transaction confirmation
ER transactions can be confirmed using standard Solana confirmation strategies:
// Option 1: sendAndConfirm (recommended)
const txHash = await providerEphemeralRollup . sendAndConfirm ( tx , [], {
skipPreflight: true ,
commitment: "confirmed" ,
});
// Option 2: Manual confirmation
const signature = await providerEphemeralRollup . connection . sendRawTransaction (
tx . serialize ()
);
await providerEphemeralRollup . connection . confirmTransaction (
signature ,
"confirmed"
);
Use skipPreflight: true for faster transaction submission. Preflight checks add unnecessary latency in ERs.
Committing state
State changes in ERs can be committed to the base layer in three ways:
Automatic commits
State automatically commits at the interval specified during delegation:
let delegate_config = DelegateConfig {
commit_frequency_ms : 30_000 , // Commit every 30 seconds
validator : None ,
};
Manual commits
You can manually commit state from within your program:
/home/daytona/workspace/source/anchor-counter/programs/anchor-counter/src/lib.rs
use ephemeral_rollups_sdk :: ephem :: commit_accounts;
pub fn commit ( ctx : Context < IncrementAndCommit >) -> Result <()> {
commit_accounts (
& ctx . accounts . payer,
vec! [ & ctx . accounts . counter . to_account_info ()],
& ctx . accounts . magic_context,
& ctx . accounts . magic_program,
) ? ;
Ok (())
}
Execute the commit from the client:
let tx = await program . methods
. commit ()
. accounts ({ payer: providerEphemeralRollup . wallet . publicKey })
. transaction ();
tx . feePayer = providerEphemeralRollup . wallet . publicKey ;
tx . recentBlockhash = (
await providerEphemeralRollup . connection . getLatestBlockhash ()
). blockhash ;
const txHash = await providerEphemeralRollup . sendAndConfirm ( tx );
Tracking commit confirmations
Use the SDK to wait for commit finalization on the base layer:
import { GetCommitmentSignature } from "@magicblock-labs/ephemeral-rollups-sdk" ;
// Execute commit on ER
const txHash = await providerEphemeralRollup . sendAndConfirm ( commitTx );
console . log ( "ER commit tx:" , txHash );
// Wait for base layer confirmation
const baseTxHash = await GetCommitmentSignature (
txHash ,
providerEphemeralRollup . connection
);
console . log ( "Base layer commit tx:" , baseTxHash );
The commit transaction executes on the ER instantly, but takes several seconds to finalize on the base layer.
Commit and undelegate
To commit final state and return the account to the base layer:
/home/daytona/workspace/source/anchor-counter/programs/anchor-counter/src/lib.rs
use ephemeral_rollups_sdk :: ephem :: commit_and_undelegate_accounts;
pub fn increment_and_undelegate ( ctx : Context < IncrementAndCommit >) -> Result <()> {
let counter = & mut ctx . accounts . counter;
counter . count += 1 ;
// Serialize the account state
counter . exit ( & crate :: ID ) ? ;
// Commit and undelegate in one operation
commit_and_undelegate_accounts (
& ctx . accounts . payer,
vec! [ & ctx . accounts . counter . to_account_info ()],
& ctx . accounts . magic_context,
& ctx . accounts . magic_program,
) ? ;
Ok (())
}
From the client:
let tx = await program . methods
. incrementAndUndelegate ()
. accounts ({ payer: providerEphemeralRollup . wallet . publicKey })
. transaction ();
tx . feePayer = providerEphemeralRollup . wallet . publicKey ;
tx . recentBlockhash = (
await providerEphemeralRollup . connection . getLatestBlockhash ()
). blockhash ;
const txHash = await providerEphemeralRollup . sendAndConfirm ( tx );
console . log ( "Undelegate tx:" , txHash );
// Wait for confirmation on base layer
const baseTxHash = await GetCommitmentSignature (
txHash ,
providerEphemeralRollup . connection
);
console . log ( "Base layer undelegate tx:" , baseTxHash );
Typical transaction latencies:
Operation Base Layer (Solana) Ephemeral Rollup Initialize 2000-3000ms N/A (done on base) Delegate 2000-3000ms N/A (done on base) Increment 2000-3000ms 50-100ms Commit N/A 50-100ms (ER) + 2000-3000ms (base confirmation) Undelegate 2000-3000ms (on ER) + wait for base confirmation N/A
Delegation and undelegation must be executed on the base layer connection, not the ER connection.
Transaction lifecycle example
Complete flow from the anchor-counter test:
// Uses base layer connection
const tx = await program . methods . initialize ()
. accounts ({ user: provider . wallet . publicKey })
. transaction ();
const txHash = await provider . sendAndConfirm ( tx );
console . log ( "2847ms (Base Layer) Initialize" );
// Uses base layer connection
const tx = await program . methods . delegate ()
. accounts ({ payer: provider . wallet . publicKey , pda: counterPDA })
. transaction ();
const txHash = await provider . sendAndConfirm ( tx );
console . log ( "2341ms (Base Layer) Delegate" );
// Wait for delegation to propagate
await new Promise (( resolve ) => setTimeout ( resolve , 3000 ));
// Uses ER connection
let tx = await program . methods . increment ()
. accounts ({ counter: counterPDA })
. transaction ();
tx . feePayer = providerER . wallet . publicKey ;
tx . recentBlockhash = ( await providerER . connection . getLatestBlockhash ()). blockhash ;
tx = await providerER . wallet . signTransaction ( tx );
const txHash = await providerER . sendAndConfirm ( tx );
console . log ( "73ms (ER) Increment" ); // 30x faster!
// Uses ER connection
let tx = await program . methods . commit ()
. accounts ({ payer: providerER . wallet . publicKey })
. transaction ();
tx . feePayer = providerER . wallet . publicKey ;
tx . recentBlockhash = ( await providerER . connection . getLatestBlockhash ()). blockhash ;
const txHash = await providerER . sendAndConfirm ( tx );
console . log ( "68ms (ER) Commit" );
// Wait for base layer confirmation
const baseTxHash = await GetCommitmentSignature ( txHash , providerER . connection );
console . log ( "2456ms (Base Layer) Commit finalized" );
// Uses ER connection for transaction, but commits to base layer
let tx = await program . methods . incrementAndUndelegate ()
. accounts ({ payer: providerER . wallet . publicKey })
. transaction ();
tx . feePayer = providerER . wallet . publicKey ;
tx . recentBlockhash = ( await providerER . connection . getLatestBlockhash ()). blockhash ;
const txHash = await providerER . sendAndConfirm ( tx );
console . log ( "81ms (ER) Undelegate" );
Best practices
Skip preflight Use skipPreflight: true to reduce latency. ER validators handle validation efficiently.
Batch operations Execute multiple operations on the ER before committing to minimize base layer costs.
Connection management Maintain separate connections for base layer and ER operations.
Error handling Handle ER connection failures gracefully and retry on the base layer if needed.
Examples
Next steps
Account delegation Learn how to delegate accounts to ERs
Ephemeral Rollups Understand the complete ER architecture