Submit single transactions

This example shows how to submit a single transaction to NextBlock using the gRPC API. We'll create a transaction with a tip and a useful instruction.

If you want to send raw signed transaction bytes over QUIC instead of gRPC, see QUIC Transaction Submission.

This example shows transaction 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]
}

// Calculate optimal tip based on priority and current tip floor
func calculateOptimalTip(priority string) uint64 {
    // This should get tip from your tip floor stream
    // Higher tips = higher priority
    // Adapt based on current network conditions
    
    // Get current tip floor data from your streaming connection
    // tipFloor := getCurrentTipFloor()
    
    // Use tip floor percentiles to determine appropriate tip
    // Example: use 25th percentile for low priority, 50th for normal, etc.
    // Return the calculated tip based on current network conditions
    
    // For now, return 0 - you must implement tip floor integration
    return 0 // Replace with actual tip floor calculation
}

// BuildAndSubmitTransaction creates and submits a transaction with proper tip
func BuildAndSubmitTransaction(
    nextblockApiClient interface{}, // Replace with your generated API client type
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
    recipientPubkey solana.PublicKey,
    transferAmount uint64,
    tipAmount uint64,
) error {
    // Get latest blockhash for transaction
    bh, err := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
    if err != nil {
        return fmt.Errorf("failed to get latest blockhash: %w", err)
    }

    // Build transaction
    txBuilder := solana.NewTransactionBuilder()
    txBuilder.SetFeePayer(signerPrivateKey.PublicKey())
    txBuilder.SetRecentBlockHash(bh.Value.Blockhash)

    // Add tip instruction (IMPORTANT: This helps prioritize your transaction)
    tipInstruction := system.NewTransferInstruction(
        tipAmount,
        signerPrivateKey.PublicKey(),
        GetRandomNextblockTipWallet(),
    ).Build()
    txBuilder.AddInstruction(tipInstruction)

    // Add your main instruction (replace with your actual business logic)
    transferInstruction := system.NewTransferInstruction(
        transferAmount,
        signerPrivateKey.PublicKey(),
        recipientPubkey,
    ).Build()
    txBuilder.AddInstruction(transferInstruction)

    // Build and sign transaction
    tx, err := txBuilder.Build()
    if err != nil {
        return 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 fmt.Errorf("failed to sign transaction: %w", err)
    }

    // Convert to base64 for submission
    base64EncodedTransaction := tx.ToBase64()

    // Configure submission options
    frontRunningProtection := false // Set to true to protect from MEV
    revertOnFail := false          // Set to true to revert if transaction fails
    disableRetries := false        // Set to true to disable automatic retries
    snipeTransaction := false      // Set to true for high-priority submission

    // Submit transaction to NextBlock
    /* Uncomment when you have the generated API client
    submitResponse, err := nextblockApiClient.PostSubmitV2(context.TODO(), &api.PostSubmitRequest{
        Transaction:            &api.TransactionMessage{Content: base64EncodedTransaction},
        SkipPreFlight:          true, // Skip preflight checks for faster submission
        SnipeTransaction:       &snipeTransaction,
        FrontRunningProtection: &frontRunningProtection,
        DisableRetries:         &disableRetries,
        RevertOnFail:          &revertOnFail,
    })
    if err != nil {
        return fmt.Errorf("failed to submit transaction: %w", err)
    }

    fmt.Printf("Transaction submitted successfully!\n")
    fmt.Printf("Signature: %s\n", submitResponse.Signature)
    fmt.Printf("UUID: %s\n", submitResponse.Uuid)
    */

    fmt.Printf("Local transaction built successfully: %s\n", tx.Signatures[0].String())
    return nil
}

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

    // Set up your configuration
    signerPrivateKey := solana.MustPrivateKeyFromBase58("your-private-key-here")
    recipientPubkey := solana.MustPublicKeyFromBase58("recipient-public-key-here")
    rpcEndpoint := "https://api.mainnet-beta.solana.com" // Use your preferred RPC
    
    // 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 transaction with dynamic tip based on priority
    err := BuildAndSubmitTransaction(
        nil, // nextblockApiClient,
        rpcClient,
        signerPrivateKey,
        recipientPubkey,
        10000,    // Transfer amount
        calculateOptimalTip("normal"),  // Dynamic tip based on tip floor
    )
    if err != nil {
        log.Fatal(err)
    }
}

Key Features Explained

Tip Amount Calculation

Use the tip floor API to determine appropriate tip amounts:

Transaction Options

  • SkipPreFlight: Set to true for faster submission (recommended)

  • FrontRunningProtection: Protects against MEV attacks

  • DisableRetries: Disable automatic retries if you handle them yourself

  • RevertOnFail: Revert transaction state if it fails

  • SnipeTransaction: High-priority submission mode

Error Handling

Always implement proper error handling:

Best Practices

  1. Always include tips: NextBlock prioritizes transactions based on tip amounts - higher tips = higher priority

  2. Use random tip wallets: Distribute load across multiple tip addresses

  3. Adapt tips to network conditions: Use the tip floor streaming API to dynamically adjust your tips based on current network conditions

  4. Choose appropriate priority levels: Use different tip levels based on your transaction urgency needs

  5. Handle errors gracefully: Implement retry logic for network issues

  6. Use appropriate RPC endpoints: Choose reliable RPC providers for blockhash retrieval

  7. Keep connections alive: Reuse gRPC connections for better performance

Last updated