Submit Batched Transactions

Submit 2-4 transactions as an atomic bundle to NextBlock using Rust. Batched transactions are processed as Jito bundles - either all transactions succeed, or none do.

Example

use solana_sdk::{
    instruction::Instruction,
    message::Message,
    pubkey::Pubkey,
    signature::{Keypair, Signature, Signer},
    system_instruction,
    transaction::Transaction,
    hash::Hash,
};
use solana_client::rpc_client::RpcClient;
use rand::seq::SliceRandom;
use std::str::FromStr;

// NextBlock tip wallets
const NEXTBLOCK_TIP_WALLETS: [&str; 8] = [
    "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
    "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc",
    "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", 
    "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
    "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
    "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
    "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
    "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
];

// Transaction builder for bundle
struct TransactionBundle {
    transactions: Vec<Transaction>,
}

impl TransactionBundle {
    fn new() -> Self {
        Self {
            transactions: Vec::new(),
        }
    }
    
    // Add transaction to bundle
    fn add_transaction(&mut self, transaction: Transaction) -> Self {
        self.transactions.push(transaction);
        self
    }
    
    // Get transactions as base64 strings
    fn to_base64_transactions(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        self.transactions
            .iter()
            .map(|tx| {
                let serialized = bincode::serialize(tx)?;
                Ok(base64::encode(serialized))
            })
            .collect()
    }
}

// Build single transaction with tip
fn build_transaction_with_tip(
    payer: &Keypair,
    recent_blockhash: Hash,
    tip_amount: u64,
    instructions: Vec<Instruction>,
) -> Result<Transaction, Box<dyn std::error::Error>> {
    let mut rng = rand::thread_rng();
    let tip_wallet_str = NEXTBLOCK_TIP_WALLETS.choose(&mut rng)
        .ok_or("No tip wallets available")?;
    let tip_wallet = Pubkey::from_str(tip_wallet_str)?;
    
    // Create tip instruction (should be first)
    let tip_instruction = system_instruction::transfer(
        &payer.pubkey(),
        &tip_wallet,
        tip_amount,
    );
    
    // Combine instructions
    let mut all_instructions = vec![tip_instruction];
    all_instructions.extend(instructions);
    
    // Build and sign transaction
    let message = Message::new(&all_instructions, Some(&payer.pubkey()));
    let mut transaction = Transaction::new_unsigned(message);
    transaction.sign(&[payer], recent_blockhash);
    
    Ok(transaction)
}

// Submit batched transactions
async fn submit_batched_transactions(
    // nextblock_client: &mut YourGeneratedApiClient,
    rpc_client: &RpcClient,
    signer: &Keypair,
) -> Result<String, Box<dyn std::error::Error>> {
    // Get recent blockhash (same for all transactions in bundle)
    let recent_blockhash = rpc_client.get_latest_blockhash()?;
    
    // Build transaction bundle
    let mut bundle = TransactionBundle::new();
    
    // Transaction 1: Setup transaction
    let setup_instructions = vec![
        system_instruction::transfer(
            &signer.pubkey(),
            &Pubkey::from_str("<recipient1-public-key>")?,
            100_000, // 0.0001 SOL
        ),
    ];
    
    let setup_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        500_000, // 0.0005 SOL tip
        setup_instructions,
    )?;
    bundle = bundle.add_transaction(setup_tx);
    
    // Transaction 2: Main operation
    let main_instructions = vec![
        system_instruction::transfer(
            &signer.pubkey(),
            &Pubkey::from_str("<recipient2-public-key>")?,
            200_000, // 0.0002 SOL
        ),
    ];
    
    let main_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        1_000_000, // 0.001 SOL tip (higher for main transaction)
        main_instructions,
    )?;
    bundle = bundle.add_transaction(main_tx);
    
    // Transaction 3: Cleanup transaction
    let cleanup_instructions = vec![
        system_instruction::transfer(
            &signer.pubkey(),
            &Pubkey::from_str("<recipient3-public-key>")?,
            50_000, // 0.00005 SOL
        ),
    ];
    
    let cleanup_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        500_000, // 0.0005 SOL tip
        cleanup_instructions,
    )?;
    bundle = bundle.add_transaction(cleanup_tx);
    
    // Convert to base64 for submission
    let base64_transactions = bundle.to_base64_transactions()?;
    
    // Print transaction signatures for tracking
    for (i, tx) in bundle.transactions.iter().enumerate() {
        println!("Transaction {} signature: {}", i + 1, tx.signatures[0]);
    }
    
    // Submit bundle to NextBlock
    /* Uncomment when you have the generated API client
    let entries: Vec<PostSubmitRequestEntry> = base64_transactions
        .into_iter()
        .map(|tx_base64| PostSubmitRequestEntry {
            transaction: Some(TransactionMessage {
                content: tx_base64,
            }),
        })
        .collect();
    
    let request = tonic::Request::new(PostSubmitBatchRequest {
        entries,
    });
    
    let response = nextblock_client.post_submit_batch_v2(request).await?;
    let submit_response = response.into_inner();
    
    println!("Batch submitted successfully!");
    println!("Bundle signature: {}", submit_response.signature);
    
    Ok(submit_response.signature)
    */
    
    println!("Batch of {} transactions built successfully!", bundle.transactions.len());
    Ok("mock-bundle-signature".to_string())
}

Arbitrage Bundle Example

// Example: Build arbitrage bundle
async fn build_arbitrage_bundle(
    // nextblock_client: &mut YourGeneratedApiClient,
    rpc_client: &RpcClient,
    signer: &Keypair,
    dex_a_address: Pubkey,
    dex_b_address: Pubkey,
    trade_amount: u64,
) -> Result<String, Box<dyn std::error::Error>> {
    let recent_blockhash = rpc_client.get_latest_blockhash()?;
    let mut bundle = TransactionBundle::new();
    
    // Transaction 1: Buy on DEX A
    let buy_instructions = vec![
        // Add your DEX-specific buy instructions here
        system_instruction::transfer(
            &signer.pubkey(),
            &dex_a_address,
            trade_amount,
        ),
    ];
    
    let buy_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        2_000_000, // Higher tip for arbitrage
        buy_instructions,
    )?;
    bundle = bundle.add_transaction(buy_tx);
    
    // Transaction 2: Sell on DEX B
    let sell_instructions = vec![
        // Add your DEX-specific sell instructions here
        system_instruction::transfer(
            &signer.pubkey(),
            &dex_b_address,
            trade_amount,
        ),
    ];
    
    let sell_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        2_000_000, // Higher tip for arbitrage
        sell_instructions,
    )?;
    bundle = bundle.add_transaction(sell_tx);
    
    println!("Arbitrage bundle with {} transactions prepared", bundle.transactions.len());
    
    // Submit the bundle
    // Implementation similar to submit_batched_transactions
    Ok("arbitrage-bundle-signature".to_string())
}

Complex DeFi Bundle Example

// Example: Complex DeFi operation bundle
async fn build_defi_operation_bundle(
    // nextblock_client: &mut YourGeneratedApiClient,
    rpc_client: &RpcClient,
    signer: &Keypair,
) -> Result<String, Box<dyn std::error::Error>> {
    let recent_blockhash = rpc_client.get_latest_blockhash()?;
    let mut bundle = TransactionBundle::new();
    
    // Transaction 1: Create token accounts
    let account_creation_instructions = vec![
        // Add token account creation instructions
        system_instruction::create_account(
            &signer.pubkey(),
            &Keypair::new().pubkey(), // New account
            1_000_000, // Rent exemption
            165,       // Token account space
            &spl_token::id(),
        ),
    ];
    
    let account_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        500_000,
        account_creation_instructions,
    )?;
    bundle = bundle.add_transaction(account_tx);
    
    // Transaction 2: Execute swap
    let swap_instructions = vec![
        // Add swap instructions (Jupiter, Raydium, etc.)
        system_instruction::transfer(
            &signer.pubkey(),
            &Pubkey::from_str("<swap-program-address>")?,
            0, // No SOL transfer for swap
        ),
    ];
    
    let swap_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        1_500_000, // Higher tip for main operation
        swap_instructions,
    )?;
    bundle = bundle.add_transaction(swap_tx);
    
    // Transaction 3: Stake or provide liquidity
    let stake_instructions = vec![
        // Add staking/liquidity instructions
        system_instruction::transfer(
            &signer.pubkey(),
            &Pubkey::from_str("<stake-pool-address>")?,
            0,
        ),
    ];
    
    let stake_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        750_000,
        stake_instructions,
    )?;
    bundle = bundle.add_transaction(stake_tx);
    
    println!("DeFi operation bundle prepared with {} transactions", bundle.transactions.len());
    Ok("defi-bundle-signature".to_string())
}

Usage Example

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize configuration
    let signer = Keypair::new(); // Use your actual keypair
    let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
    
    // Connect to NextBlock (see connection.md)
    // let config = NextBlockConfig::default();
    // let mut nextblock_client = create_nextblock_client(config).await?;
    
    // Submit basic batch
    let bundle_signature = submit_batched_transactions(
        // &mut nextblock_client,
        &rpc_client,
        &signer,
    ).await?;
    
    println!("Batch submitted with signature: {}", bundle_signature);
    
    // Submit arbitrage bundle
    let arb_signature = build_arbitrage_bundle(
        // &mut nextblock_client,
        &rpc_client,
        &signer,
        Pubkey::from_str("<dex-a-address>")?,
        Pubkey::from_str("<dex-b-address>")?,
        1_000_000, // 0.001 SOL
    ).await?;
    
    println!("Arbitrage bundle: {}", arb_signature);
    
    // Submit complex DeFi bundle
    let defi_signature = build_defi_operation_bundle(
        // &mut nextblock_client,
        &rpc_client,
        &signer,
    ).await?;
    
    println!("DeFi bundle: {}", defi_signature);
    
    Ok(())
}

Bundle Best Practices

Size and Composition

  • Bundle size: 2-4 transactions (optimal: 2-3)

  • Transaction order: Setup → Main operations → Cleanup

  • Compute limits: Stay within bundle compute unit limits

Tip Strategy

  • Progressive tipping: Higher tips for more critical transactions

  • Bundle coherence: All transactions should have meaningful tips

  • Market conditions: Adjust tips based on network congestion

Error Handling

// Validate bundle before submission
fn validate_bundle(bundle: &TransactionBundle) -> Result<(), &'static str> {
    if bundle.transactions.is_empty() {
        return Err("Bundle cannot be empty");
    }
    
    if bundle.transactions.len() > 4 {
        return Err("Bundle cannot contain more than 4 transactions");
    }
    
    if bundle.transactions.len() < 2 {
        return Err("Bundle must contain at least 2 transactions");
    }
    
    // Check for signature duplicates
    let mut signatures = std::collections::HashSet::new();
    for tx in &bundle.transactions {
        if !signatures.insert(tx.signatures[0]) {
            return Err("Duplicate transaction signatures in bundle");
        }
    }
    
    Ok(())
}

Common Use Cases

  1. Arbitrage: Multi-DEX price differences

  2. DeFi operations: Account setup → Trade → Stake

  3. NFT operations: Create → Mint → Transfer → List

  4. Token operations: Create mint → Create accounts → Transfer

  5. Liquidations: Detect → Execute → Collect rewards

Bundle Failure Prevention

  • Account validation: Ensure all accounts exist and have sufficient balance

  • Instruction compatibility: Avoid conflicting program interactions

  • Blockhash freshness: Use recent blockhashes (valid ~60 seconds)

  • Compute budget: Monitor total compute units across all transactions

Last updated