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.

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("Batch of %d transactions built successfully!\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("fra.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

// Check if bundle was included atomically
func CheckBundleStatus(signature string) {
    // Implement bundle status checking
    // All transactions should have the same block slot if successful
}

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