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.

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("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("fra.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:

// Get current tip floor data (see tip-floor-stream.md for streaming version)
func GetOptimalTipAmount(nextblockApiClient interface{}) uint64 {
    // This would call the tip floor API to get current percentiles
    // For now, use a reasonable default
    return 1000000 // 0.001 SOL
}

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:

if err != nil {
    // Log the error with context
    log.Printf("Transaction submission failed: %v", err)
    
    // Implement retry logic if needed
    // Check if error is retryable
    // Wait and retry with exponential backoff
}

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