Submit batched transactions

Submit 2-4 transactions as an atomic bundle to NextBlock. Batched transactions are processed as Jito bundles, meaning either all transactions succeed and land, or none of them do.

This example shows bundle construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from nextblock-proto.

Example

package main

import (
    "context"
    "fmt"
    "log"
    "math/rand"
    "time"
    
    "github.com/gagliardetto/solana-go"
    "github.com/gagliardetto/solana-go/programs/system"
    "github.com/gagliardetto/solana-go/rpc"
    // Import your generated proto client
    // "path/to/your/generated/api"
)

// NextBlock tip wallets - use these for tipping
var NextblockTipWallets = []solana.PublicKey{
    solana.MustPublicKeyFromBase58("NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid"),
    solana.MustPublicKeyFromBase58("nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"),
    solana.MustPublicKeyFromBase58("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"),
    solana.MustPublicKeyFromBase58("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"),
    solana.MustPublicKeyFromBase58("NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X"),
    solana.MustPublicKeyFromBase58("NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb"),
    solana.MustPublicKeyFromBase58("neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At"),
    solana.MustPublicKeyFromBase58("nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG"),
}

// GetRandomNextblockTipWallet returns a random tip wallet for load balancing
func GetRandomNextblockTipWallet() solana.PublicKey {
    randVal := rand.IntN(len(NextblockTipWallets))
    return NextblockTipWallets[randVal]
}

// BuildTransaction creates a single transaction with tip and instructions
func BuildTransaction(
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
    blockhash solana.Hash,
    tipAmount uint64,
    instructions []solana.Instruction,
) (*solana.Transaction, error) {
    txBuilder := solana.NewTransactionBuilder()
    txBuilder.SetFeePayer(signerPrivateKey.PublicKey())
    txBuilder.SetRecentBlockHash(blockhash)

    // Add tip instruction first (recommended practice)
    tipInstruction := system.NewTransferInstruction(
        tipAmount,
        signerPrivateKey.PublicKey(),
        GetRandomNextblockTipWallet(),
    ).Build()
    txBuilder.AddInstruction(tipInstruction)

    // Add all provided instructions
    for _, instruction := range instructions {
        txBuilder.AddInstruction(instruction)
    }

    // Build and sign transaction
    tx, err := txBuilder.Build()
    if err != nil {
        return nil, fmt.Errorf("failed to build transaction: %w", err)
    }

    _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
        if key.Equals(signerPrivateKey.PublicKey()) {
            return &signerPrivateKey
        }
        return nil
    })
    if err != nil {
        return nil, fmt.Errorf("failed to sign transaction: %w", err)
    }

    return tx, nil
}

// BuildAndSubmitBatchTransactions creates and submits multiple transactions as a bundle
func BuildAndSubmitBatchTransactions(
    nextblockApiClient interface{}, // Replace with your generated API client type
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
) error {
    // Get latest blockhash (same for all transactions in bundle)
    bh, err := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
    if err != nil {
        return fmt.Errorf("failed to get latest blockhash: %w", err)
    }

    var transactions []*solana.Transaction
    
    // Transaction 1: Setup transaction
    setupInstructions := []solana.Instruction{
        system.NewTransferInstruction(
            100000, // 0.0001 SOL
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("recipient1-public-key-here"),
        ).Build(),
    }
    
    tx1, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        500000, // 0.0005 SOL tip
        setupInstructions,
    )
    if err != nil {
        return fmt.Errorf("failed to build transaction 1: %w", err)
    }
    transactions = append(transactions, tx1)

    // Transaction 2: Main operation
    mainInstructions := []solana.Instruction{
        system.NewTransferInstruction(
            200000, // 0.0002 SOL
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("recipient2-public-key-here"),
        ).Build(),
    }
    
    tx2, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        1000000, // 0.001 SOL tip (higher for main transaction)
        mainInstructions,
    )
    if err != nil {
        return fmt.Errorf("failed to build transaction 2: %w", err)
    }
    transactions = append(transactions, tx2)

    // Transaction 3: Cleanup transaction (optional)
    cleanupInstructions := []solana.Instruction{
        system.NewTransferInstruction(
            50000, // 0.00005 SOL
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("recipient3-public-key-here"),
        ).Build(),
    }
    
    tx3, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        500000, // 0.0005 SOL tip
        cleanupInstructions,
    )
    if err != nil {
        return fmt.Errorf("failed to build transaction 3: %w", err)
    }
    transactions = append(transactions, tx3)

    // Convert transactions to base64 for submission
    var entries []*interface{} // Replace with your generated API types
    
    for i, tx := range transactions {
        base64EncodedTransaction := tx.ToBase64()
        fmt.Printf("Transaction %d signature: %s\n", i+1, tx.Signatures[0].String())
        
        // Create entry for batch submission
        /* Uncomment when you have the generated API client
        entry := &api.PostSubmitRequestEntry{
            Transaction: &api.TransactionMessage{
                Content: base64EncodedTransaction,
            },
        }
        entries = append(entries, entry)
        */
        _ = base64EncodedTransaction // Placeholder to avoid unused variable error
    }

    // Submit batch to NextBlock
    /* Uncomment when you have the generated API client
    submitResponse, err := nextblockApiClient.PostSubmitBatchV2(context.TODO(), &api.PostSubmitBatchRequest{
        Entries: entries,
    })
    if err != nil {
        return fmt.Errorf("failed to submit batch: %w", err)
    }

    fmt.Printf("Batch submitted successfully!\n")
    fmt.Printf("Bundle signature: %s\n", submitResponse.Signature)
    */

    fmt.Printf("Local bundle prepared with %d transactions.\n", len(transactions))
    return nil
}

// Example: Arbitrage Bundle
func BuildArbitrageBundleExample(
    nextblockApiClient interface{},
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
) error {
    // Get blockhash
    bh, err := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
    if err != nil {
        return fmt.Errorf("failed to get latest blockhash: %w", err)
    }

    var transactions []*solana.Transaction

    // Transaction 1: Buy on DEX A
    buyInstructions := []solana.Instruction{
        // Add your DEX buy instructions here
        system.NewTransferInstruction(
            1000000, // Placeholder instruction
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("dex-a-address"),
        ).Build(),
    }
    
    buyTx, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        2000000, // Higher tip for arbitrage
        buyInstructions,
    )
    if err != nil {
        return err
    }
    transactions = append(transactions, buyTx)

    // Transaction 2: Sell on DEX B
    sellInstructions := []solana.Instruction{
        // Add your DEX sell instructions here
        system.NewTransferInstruction(
            1000000, // Placeholder instruction
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("dex-b-address"),
        ).Build(),
    }
    
    sellTx, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        2000000, // Higher tip for arbitrage
        sellInstructions,
    )
    if err != nil {
        return err
    }
    transactions = append(transactions, sellTx)

    fmt.Printf("Arbitrage bundle with %d transactions prepared\n", len(transactions))
    // Submit logic similar to above...
    return nil
}

func main() {
    // Initialize random seed
    rand.Seed(time.Now().UnixNano())

    // Configuration
    signerPrivateKey := solana.MustPrivateKeyFromBase58("your-private-key-here")
    rpcEndpoint := "https://api.mainnet-beta.solana.com"
    
    // Initialize RPC client
    rpcClient := rpc.New(rpcEndpoint)

    // Connect to NextBlock (see connection.md for full setup)
    // conn, err := ConnectToNextblock("frankfurt.nextblock.io:443", "your-api-key", true)
    // if err != nil {
    //     log.Fatal(err)
    // }
    // defer conn.Close()
    // nextblockApiClient := api.NewApiClient(conn)

    // Submit batch transactions
    err := BuildAndSubmitBatchTransactions(
        nil, // nextblockApiClient,
        rpcClient,
        signerPrivateKey,
    )
    if err != nil {
        log.Fatal(err)
    }

    // Example: Submit arbitrage bundle
    err = BuildArbitrageBundleExample(
        nil, // nextblockApiClient,
        rpcClient,
        signerPrivateKey,
    )
    if err != nil {
        log.Fatal(err)
    }
}

Batch Transaction Best Practices

Bundle Size Limits

  • Minimum: 2 transactions

  • Maximum: 4 transactions

  • Optimal: 2-3 transactions for better success rates

Transaction Ordering

  1. Setup transactions: Account creation, token account setup

  2. Main transactions: Core business logic

  3. Cleanup transactions: Close accounts, collect fees

Tip Strategy for Bundles

  • Higher tips: Use higher tips for atomic operations like arbitrage

  • Distributed tips: Each transaction should have its own tip

  • Progressive tipping: Increase tips for more important transactions

Error Handling

Common Use Cases

  1. Arbitrage: Buy low on one DEX, sell high on another

  2. Complex DeFi operations: Setup → Trade → Cleanup

  3. NFT operations: Create → Mint → Transfer

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

Bundle Failure Scenarios

  • Insufficient balance: Ensure all accounts have enough SOL

  • Account conflicts: Avoid write conflicts between transactions

  • Instruction limits: Keep total compute units under limits

  • Blockhash expiry: Use fresh blockhashes (valid for ~60 seconds)

Last updated