Submit Batched Transactions

Submit 2-4 transactions as an atomic bundle to NextBlock using JavaScript/TypeScript.

import { 
  Connection, 
  Keypair, 
  PublicKey, 
  SystemProgram, 
  Transaction 
} from '@solana/web3.js';

// Transaction bundle builder
class TransactionBundle {
  private transactions: Transaction[] = [];
  private readonly maxSize: number = 4;
  private readonly minSize: number = 2;

  addTransaction(transaction: Transaction): TransactionBundle {
    if (this.transactions.length >= this.maxSize) {
      throw new Error(`Bundle cannot contain more than ${this.maxSize} transactions`);
    }
    
    this.transactions.push(transaction);
    return this;
  }

  validate(): boolean {
    if (this.transactions.length < this.minSize) {
      throw new Error(`Bundle must contain at least ${this.minSize} transactions`);
    }

    if (this.transactions.length > this.maxSize) {
      throw new Error(`Bundle cannot contain more than ${this.maxSize} transactions`);
    }

    // Check for duplicate signatures
    const signatures = new Set();
    for (const tx of this.transactions) {
      const sig = tx.signature?.toString();
      if (sig && signatures.has(sig)) {
        throw new Error('Duplicate transaction signatures in bundle');
      }
      if (sig) signatures.add(sig);
    }

    return true;
  }

  toBase64Transactions(): string[] {
    this.validate();
    
    return this.transactions.map(tx => {
      const serialized = tx.serialize();
      return serialized.toString('base64');
    });
  }

  getSignatures(): string[] {
    return this.transactions
      .map(tx => tx.signature?.toString())
      .filter((sig): sig is string => sig !== undefined);
  }

  get size(): number {
    return this.transactions.length;
  }
}

// Build single transaction with tip
async function buildTransactionWithTip(
  connection: Connection,
  payer: Keypair,
  tipAmount: number,
  instructions: any[]
): Promise<Transaction> {
  const { blockhash } = await connection.getLatestBlockhash('finalized');
  
  // Random tip wallet for load balancing
  const tipWallets = [
    'NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid',
    'nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc',
    'NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE',
    'NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2',
  ];
  
  const tipWallet = new PublicKey(tipWallets[Math.floor(Math.random() * tipWallets.length)]);
  
  // Create tip instruction (should be first)
  const tipInstruction = SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: tipWallet,
    lamports: tipAmount,
  });

  // Create and sign transaction
  const transaction = new Transaction({
    recentBlockhash: blockhash,
    feePayer: payer.publicKey,
  });

  transaction.add(tipInstruction);
  instructions.forEach(instruction => transaction.add(instruction));
  transaction.sign(payer);

  return transaction;
}

// Submit batched transactions
async function submitBatchedTransactions(
  // nextblockClient: any, // Your generated gRPC client
  connection: Connection,
  signer: Keypair,
  transactionSpecs: Array<{
    instructions: any[];
    tipAmount: number;
  }>
): Promise<string> {
  // Build transaction bundle
  const bundle = new TransactionBundle();
  
  for (const { instructions, tipAmount } of transactionSpecs) {
    const transaction = await buildTransactionWithTip(
      connection,
      signer,
      tipAmount,
      instructions
    );
    bundle.addTransaction(transaction);
  }

  // Get base64 transactions for submission
  const base64Transactions = bundle.toBase64Transactions();
  const signatures = bundle.getSignatures();

  // Log transaction signatures
  signatures.forEach((sig, i) => {
    console.log(`Transaction ${i + 1} signature: ${sig}`);
  });

  // Submit bundle to NextBlock
  /* Uncomment when you have the generated gRPC client
  const entries = base64Transactions.map(base64Tx => ({
    transaction: { content: base64Tx }
  }));

  const request = { entries };
  const response = await nextblockClient.postSubmitBatchV2(request);

  console.log('Batch submitted successfully!');
  console.log(`Bundle signature: ${response.signature}`);

  return response.signature;
  */

  // Mock response for demonstration
  const bundleSignature = `bundle-${bundle.size}-transactions`;
  console.log(`Batch of ${bundle.size} transactions built successfully!`);
  return bundleSignature;
}

// Build arbitrage bundle
async function buildArbitrageBundle(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair,
  dexAAddress: PublicKey,
  dexBAddress: PublicKey,
  tradeAmount: number
): Promise<string> {
  // Transaction 1: Buy on DEX A
  const buyInstructions = [
    // Add your DEX-specific buy instructions here
    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: dexAAddress,
      lamports: tradeAmount,
    })
  ];

  // Transaction 2: Sell on DEX B
  const sellInstructions = [
    // Add your DEX-specific sell instructions here
    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: dexBAddress,
      lamports: tradeAmount,
    })
  ];

  const transactionSpecs = [
    { instructions: buyInstructions, tipAmount: 2_000_000 },   // Higher tip for arbitrage
    { instructions: sellInstructions, tipAmount: 2_000_000 },  // Higher tip for arbitrage
  ];

  return await submitBatchedTransactions(
    // nextblockClient,
    connection,
    signer,
    transactionSpecs
  );
}

// Build complex DeFi operation bundle
async function buildDeFiOperationBundle(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair
): Promise<string> {
  // Transaction 1: Setup - Create token accounts
  const setupInstructions = [
    // Add token account creation instructions
    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: new PublicKey('<token-program-address>'),
      lamports: 1_000_000 // Rent exemption
    })
  ];

  // Transaction 2: Main operation - Execute swap
  const swapInstructions = [
    // Add swap instructions (Jupiter, Raydium, etc.)
    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: new PublicKey('<swap-program-address>'),
      lamports: 0 // No SOL transfer for swap
    })
  ];

  // Transaction 3: Cleanup - Stake or provide liquidity
  const stakeInstructions = [
    // Add staking/liquidity instructions
    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: new PublicKey('<stake-pool-address>'),
      lamports: 0
    })
  ];

  const transactionSpecs = [
    { instructions: setupInstructions, tipAmount: 500_000 },    // Setup tip
    { instructions: swapInstructions, tipAmount: 1_500_000 },   // Main operation tip
    { instructions: stakeInstructions, tipAmount: 750_000 },    // Cleanup tip
  ];

  return await submitBatchedTransactions(
    // nextblockClient,
    connection,
    signer,
    transactionSpecs
  );
}

// Bundle optimizer
class BundleOptimizer {
  private tipMultipliers = {
    setup: 0.5,      // Lower priority
    main: 1.5,       // Higher priority
    cleanup: 0.75,   // Medium priority
    arbitrage: 2.0,  // Highest priority
  };

  optimizeTips(baseTip: number, transactionTypes: string[]): number[] {
    return transactionTypes.map(txType => {
      const multiplier = this.tipMultipliers[txType as keyof typeof this.tipMultipliers] || 1.0;
      return Math.floor(baseTip * multiplier);
    });
  }

  reorderTransactions<T>(
    transactions: T[], 
    transactionTypes: string[]
  ): { transactions: T[]; types: string[] } {
    // Priority order: setup -> main -> cleanup
    const priorityOrder = { setup: 1, main: 2, cleanup: 3, arbitrage: 0 };
    
    const pairs = transactions.map((tx, i) => ({
      transaction: tx,
      type: transactionTypes[i],
      priority: priorityOrder[transactionTypes[i] as keyof typeof priorityOrder] || 2
    }));

    pairs.sort((a, b) => a.priority - b.priority);

    return {
      transactions: pairs.map(p => p.transaction),
      types: pairs.map(p => p.type)
    };
  }
}

// Bundle status tracking
interface BundleStatus {
  bundleId: string;
  transactionCount: number;
  submittedAt: number;
  signatures: string[];
  status: 'pending' | 'confirmed' | 'failed';
}

class BundleTracker {
  private bundles: Map<string, BundleStatus> = new Map();

  trackBundle(bundleStatus: BundleStatus): void {
    this.bundles.set(bundleStatus.bundleId, bundleStatus);
  }

  async checkBundleStatus(
    bundleId: string,
    connection: Connection
  ): Promise<BundleStatus | null> {
    const bundleStatus = this.bundles.get(bundleId);
    if (!bundleStatus) return null;

    // Check if all transactions are confirmed
    let confirmedCount = 0;
    for (const signature of bundleStatus.signatures) {
      try {
        const status = await connection.getSignatureStatus(signature);
        if (status.value?.confirmationStatus) {
          confirmedCount++;
        }
      } catch (error) {
        console.error(`Failed to check signature ${signature}:`, error);
      }
    }

    // Update bundle status
    if (confirmedCount === bundleStatus.transactionCount) {
      bundleStatus.status = 'confirmed';
    } else if (Date.now() - bundleStatus.submittedAt > 60000) { // Timeout after 60 seconds
      bundleStatus.status = 'failed';
    }

    return bundleStatus;
  }

  getBundleStatus(bundleId: string): BundleStatus | null {
    return this.bundles.get(bundleId) || null;
  }
}

Usage Examples

async function main() {
  // Initialize Solana connection
  const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
  const signer = Keypair.generate(); // Use your actual keypair

  // Connect to NextBlock (see connection.md)
  // const config = configFromEnv();
  // const manager = new NextBlockConnectionManager(config);
  // await manager.connect();
  // const nextblockClient = manager.grpcClient;

  try {
    // Example 1: Basic batch submission
    console.log('1. Basic batch submission:');
    const transactionSpecs = [
      // Setup transaction
      {
        instructions: [SystemProgram.transfer({
          fromPubkey: signer.publicKey,
          toPubkey: new PublicKey('<recipient1>'),
          lamports: 100_000,
        })],
        tipAmount: 500_000, // 0.0005 SOL tip
      },
      // Main transaction
      {
        instructions: [SystemProgram.transfer({
          fromPubkey: signer.publicKey,
          toPubkey: new PublicKey('<recipient2>'),
          lamports: 200_000,
        })],
        tipAmount: 1_000_000, // 0.001 SOL tip
      },
      // Cleanup transaction
      {
        instructions: [SystemProgram.transfer({
          fromPubkey: signer.publicKey,
          toPubkey: new PublicKey('<recipient3>'),
          lamports: 50_000,
        })],
        tipAmount: 500_000, // 0.0005 SOL tip
      },
    ];

    const bundleSignature = await submitBatchedTransactions(
      // nextblockClient,
      connection,
      signer,
      transactionSpecs
    );
    console.log(`Basic batch: ${bundleSignature}\n`);

    // Example 2: Arbitrage bundle
    console.log('2. Arbitrage bundle:');
    const arbSignature = await buildArbitrageBundle(
      // nextblockClient,
      connection,
      signer,
      new PublicKey('<dex-a-address>'),
      new PublicKey('<dex-b-address>'),
      1_000_000 // 0.001 SOL trade
    );
    console.log(`Arbitrage bundle: ${arbSignature}\n`);

    // Example 3: Optimized DeFi bundle
    console.log('3. DeFi operation bundle:');
    const defiSignature = await buildDeFiOperationBundle(
      // nextblockClient,
      connection,
      signer
    );
    console.log(`DeFi bundle: ${defiSignature}\n`);

    // Example 4: Bundle optimization
    console.log('4. Bundle optimization:');
    const optimizer = new BundleOptimizer();
    const baseTip = 1_000_000;
    const transactionTypes = ['setup', 'main', 'cleanup'];
    const optimizedTips = optimizer.optimizeTips(baseTip, transactionTypes);

    console.log(`Optimized tips: ${optimizedTips}`);

    // Example 5: Bundle tracking
    console.log('5. Bundle tracking:');
    const tracker = new BundleTracker();
    
    const bundleStatus: BundleStatus = {
      bundleId: 'bundle-123',
      transactionCount: 3,
      submittedAt: Date.now(),
      signatures: ['sig1', 'sig2', 'sig3'],
      status: 'pending'
    };
    
    tracker.trackBundle(bundleStatus);
    
    // Check status after some time
    setTimeout(async () => {
      const status = await tracker.checkBundleStatus('bundle-123', connection);
      console.log('Bundle status:', status);
    }, 5000);

  } catch (error) {
    console.error('Error in main:', error);
  } finally {
    // Clean up connection
    // await manager.disconnect();
  }
}

if (require.main === module) {
  main().catch(console.error);
}

export {
  TransactionBundle,
  submitBatchedTransactions,
  buildArbitrageBundle,
  buildDeFiOperationBundle,
  BundleOptimizer,
  BundleTracker,
};

Best Practices

  1. Bundle size limits: Keep bundles between 2-4 transactions for optimal success rates

  2. Transaction ordering: Setup → Main operations → Cleanup

  3. Progressive tipping: Use higher tips for more critical transactions

  4. Error handling: Validate bundles before submission

  5. Performance monitoring: Track bundle success rates and timing

  6. Tip optimization: Adjust tips based on transaction importance and network conditions

  7. Use TypeScript: Leverage type safety for complex bundle operations

Last updated