Skip to main content
MonitorTransaction is a server-streaming RPC — the server pushes status updates until the transaction reaches a terminal state. This guide covers production usage: understanding the full lifecycle, handling every terminal outcome, and reconnecting reliably when the stream drops. For request and response field documentation, see the MonitorTransaction reference.

Stream lifecycle

When you call MonitorTransaction, the server begins watching the transaction’s on-chain status and emits a new response each time it advances. The stream closes automatically when the transaction reaches a terminal state.
StatusTerminal?Meaning
TRANSACTION_STATUS_RECEIVEDNoReceived by a validator
TRANSACTION_STATUS_PROCESSEDNoProcessed (Processed commitment)
TRANSACTION_STATUS_CONFIRMEDNoConfirmed (Confirmed commitment)
TRANSACTION_STATUS_FINALIZEDYesFinalized — transaction succeeded
TRANSACTION_STATUS_FAILEDYesOn-chain execution failed
TRANSACTION_STATUS_DROPPEDYesNot processed; blockhash may still be valid
TRANSACTION_STATUS_TIMEOUTYesMonitoring window expired
TIMEOUT means the monitoring window expired (default: 60 seconds), not that the transaction failed. The transaction may still be processing on-chain.DROPPED means the transaction was not processed by any validator. If the blockhash has not yet expired, you can resubmit the same transaction.

Basic stream consumption

The minimal pattern to read a MonitorTransaction stream and handle terminal states:
stream, err := client.MonitorTransaction(ctx, &transaction_v1.MonitorTransactionRequest{
    Signature: "YourTransactionSignature1111111111111111111",
})
if err != nil {
    log.Fatal(err)
}

for {
    resp, err := stream.Recv()
    if err == io.EOF {
        break // Stream closed by server
    }
    if err != nil {
        log.Printf("Stream error: %v", err)
        break
    }

    fmt.Printf("Status: %v (slot %d)\n", resp.Status, resp.Slot)

    switch resp.Status {
    case transaction_v1.TransactionStatus_TRANSACTION_STATUS_FINALIZED:
        fmt.Println("Transaction finalized")
        return
    case transaction_v1.TransactionStatus_TRANSACTION_STATUS_FAILED:
        fmt.Printf("Transaction failed: %s\n", resp.ErrorMessage)
        return
    case transaction_v1.TransactionStatus_TRANSACTION_STATUS_DROPPED:
        fmt.Println("Transaction dropped — consider resubmitting")
        return
    case transaction_v1.TransactionStatus_TRANSACTION_STATUS_TIMEOUT:
        fmt.Println("Monitoring timed out — check GetTransaction for current status")
        return
    }
}

Handling terminal states

Each terminal state requires a different response. FINALIZED — The transaction succeeded and is permanent. No further action needed. FAILED — The transaction executed on-chain but failed (for example, insufficient balance or a program error). Check error_message for details. See the Error Reference for error codes. Do not resubmit the same transaction — the failure is deterministic. DROPPED — The transaction was not processed. Check whether the blockhash has expired using CheckIfTransactionIsExpired. If the blockhash is still valid, resubmit with SubmitTransaction. If expired, recompile with CompileTransaction to get a fresh blockhash, then re-sign and resubmit. TIMEOUT — The monitoring window expired (default: 60 seconds). The transaction may still be processing on-chain. Two options:
  1. Call GetTransaction to check current status.
  2. Start a new MonitorTransaction call with the same signature.
The reconnection section below shows how to handle both stream errors and TIMEOUT automatically in production code.

Production pattern: reconnect with backoff

In production, two things can go wrong beyond terminal states: the gRPC stream itself can drop due to a network error, or TIMEOUT can fire before the transaction finalizes. The pattern below handles both — it retries on stream errors with exponential backoff and restarts monitoring on TIMEOUT.
import (
    "context"
    "fmt"
    "io"
    "log"
    "time"

    transaction_v1 "github.com/meshtrade/protochain/lib/go/protochain/solana/transaction/v1"
)

func monitorWithRetry(
    ctx context.Context,
    client transaction_v1.TransactionServiceClient,
    signature string,
    maxAttempts int,
    baseDuration time.Duration,
) error {
    maxBackoff := 30 * time.Second

    for attempt := 0; attempt < maxAttempts; attempt++ {
        stream, err := client.MonitorTransaction(ctx, &transaction_v1.MonitorTransactionRequest{
            Signature: signature,
        })
        if err != nil {
            log.Printf("Failed to open stream (attempt %d): %v", attempt+1, err)
            backoff := baseDuration * time.Duration(1<<attempt)
            if backoff > maxBackoff {
                backoff = maxBackoff
            }
            time.Sleep(backoff)
            continue
        }

        streamErr := readStream(stream)
        switch streamErr {
        case nil:
            return nil // FINALIZED — success
        case errFailed:
            return streamErr // FAILED — deterministic, do not retry
        case errDropped:
            return streamErr // DROPPED — caller decides whether to resubmit
        case errTimeout:
            // TIMEOUT — monitoring window expired, retry with backoff
            log.Printf("Monitoring timed out (attempt %d), retrying...", attempt+1)
        default:
            // Network/stream error — retry with backoff
            log.Printf("Stream error (attempt %d): %v", attempt+1, streamErr)
        }

        backoff := baseDuration * time.Duration(1<<attempt)
        if backoff > maxBackoff {
            backoff = maxBackoff
        }
        time.Sleep(backoff)
    }

    return fmt.Errorf("max retries exceeded after %d attempts", maxAttempts)
}

var errFailed = fmt.Errorf("transaction failed on-chain")
var errDropped = fmt.Errorf("transaction dropped")
var errTimeout = fmt.Errorf("monitoring timeout")

func readStream(stream transaction_v1.TransactionService_MonitorTransactionClient) error {
    for {
        resp, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        switch resp.Status {
        case transaction_v1.TransactionStatus_TRANSACTION_STATUS_FINALIZED:
            log.Printf("Finalized at slot %d", resp.Slot)
            return nil
        case transaction_v1.TransactionStatus_TRANSACTION_STATUS_FAILED:
            return fmt.Errorf("%w: %s", errFailed, resp.ErrorMessage)
        case transaction_v1.TransactionStatus_TRANSACTION_STATUS_DROPPED:
            return errDropped
        case transaction_v1.TransactionStatus_TRANSACTION_STATUS_TIMEOUT:
            return errTimeout
        }
    }
}

Choosing a commitment level

By default, MonitorTransaction monitors until CONFIRMED. To wait for FINALIZED (a stronger finality guarantee), set commitment_level to COMMITMENT_LEVEL_FINALIZED in the request. For most applications, CONFIRMED is sufficient — finalization is rarely missed after confirmation.
stream, err := client.MonitorTransaction(ctx, &transaction_v1.MonitorTransactionRequest{
    Signature:       "YourTransactionSignature1111111111111111111",
    CommitmentLevel: shared_v1.CommitmentLevel_COMMITMENT_LEVEL_FINALIZED,
    TimeoutSeconds:  120,
})

Including execution logs

Set include_logs: true in the request to receive program execution logs in each status update. Useful for debugging failed transactions. Logs are populated in MonitorTransactionResponse.logs on PROCESSED and later statuses.
stream, err := client.MonitorTransaction(ctx, &transaction_v1.MonitorTransactionRequest{
    Signature:   "YourTransactionSignature1111111111111111111",
    IncludeLogs: true,
})
// ...
for {
    resp, err := stream.Recv()
    // ...
    if len(resp.Logs) > 0 {
        fmt.Printf("Logs: %v\n", resp.Logs)
    }
}

Next steps