|
| 1 | +--- |
| 2 | +section: cre |
| 3 | +title: "Verifying Transaction Status" |
| 4 | +sdkLang: "go" |
| 5 | +pageId: "guides-workflow-evm-verifying-transaction-status" |
| 6 | +date: Last Modified |
| 7 | +metadata: |
| 8 | + description: "Verify consumer contract execution in Go: learn to check both transaction success and your contract's onReport() execution status." |
| 9 | + datePublished: "2025-12-08" |
| 10 | + lastModified: "2025-12-08" |
| 11 | +--- |
| 12 | + |
| 13 | +import { Aside, ClickToZoom } from "@components" |
| 14 | + |
| 15 | +When your workflow writes data to the blockchain, you can verify both that the transaction was mined and that your consumer contract successfully processed the data. This guide explains how to properly check both levels of execution. |
| 16 | + |
| 17 | +<Aside type="note" title="Prerequisites"> |
| 18 | + This guide assumes you're already familiar with how CRE's onchain write process works. If you haven't read it yet, |
| 19 | + start with [Onchain Write Overview](/cre/guides/workflow/using-evm-client/onchain-write/overview-go) to understand the |
| 20 | + secure write flow. |
| 21 | +</Aside> |
| 22 | + |
| 23 | +## Why two levels of verification? |
| 24 | + |
| 25 | +Your workflow's data goes through a two-tier transaction model: |
| 26 | + |
| 27 | +1. **Outer Transaction**: Your workflow → `KeystoneForwarder` contract (on the blockchain) |
| 28 | +1. **Inner Execution**: `KeystoneForwarder` → Your [consumer contract](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts)'s `onReport()` function |
| 29 | + |
| 30 | +A common mistake is only checking the outer transaction status. **The transaction can succeed while your consumer contract's `onReport()` function reverts.** |
| 31 | + |
| 32 | +## Understanding the status fields |
| 33 | + |
| 34 | +When you call `WriteReport()` on the EVM client and await the promise, you receive a `WriteReportReply` struct. This struct contains the complete results of your write operation, including two status indicators: |
| 35 | + |
| 36 | +| Field | What it checks | Success means | Failure means | |
| 37 | +| --------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | |
| 38 | +| `TxStatus` | Was the transaction mined on the blockchain? | Forwarder received and processed the report | Network issues, insufficient gas, or forwarder rejected the report | |
| 39 | +| `ReceiverContractExecutionStatus` | Did YOUR consumer contract's `onReport()` complete without reverting? | All validation passed (if any), `_processReport()` executed successfully | Forwarder validation failed, workflow ID mismatch, or your business logic reverted | |
| 40 | + |
| 41 | +<Aside type="caution" title="Simulation Limitation"> |
| 42 | + The `receiverContractExecutionStatus` field is currently not populated during workflow simulation. This verification |
| 43 | + only works in production environments. During simulation, you can only verify that the transaction to the Forwarder |
| 44 | + succeeded (`txStatus`), but cannot detect if your consumer contract's `onReport()` function reverted. |
| 45 | +</Aside> |
| 46 | + |
| 47 | +## The complete verification pattern |
| 48 | + |
| 49 | +### Only checks outer transaction |
| 50 | + |
| 51 | +```go |
| 52 | +resp, err := writePromise.Await() |
| 53 | +if err != nil { |
| 54 | + return fmt.Errorf("write failed: %w", err) |
| 55 | +} |
| 56 | + |
| 57 | +// INCOMPLETE: Only verifies the transaction was mined |
| 58 | +if resp.TxStatus == evm.TxStatus_TX_STATUS_SUCCESS { |
| 59 | + logger.Info("Transaction succeeded") |
| 60 | + return nil |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +**Problem**: Your consumer contract could have reverted, but you'd never know because you only checked if the transaction was mined. |
| 65 | + |
| 66 | +### Checks both levels |
| 67 | + |
| 68 | +```go |
| 69 | +resp, err := writePromise.Await() |
| 70 | +if err != nil { |
| 71 | + return fmt.Errorf("write failed: %w", err) |
| 72 | +} |
| 73 | + |
| 74 | +// Step 1: Check outer transaction status |
| 75 | +if resp.TxStatus != evm.TxStatus_TX_STATUS_SUCCESS { |
| 76 | + return fmt.Errorf("transaction failed with status: %v", resp.TxStatus) |
| 77 | +} |
| 78 | + |
| 79 | +// Step 2: Check consumer contract execution status |
| 80 | +if resp.ReceiverContractExecutionStatus != nil && |
| 81 | + *resp.ReceiverContractExecutionStatus == evm.ReceiverContractExecutionStatus_RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED { |
| 82 | + logger.Error("Consumer contract reverted", |
| 83 | + "error", resp.GetErrorMessage(), |
| 84 | + "txHash", common.BytesToHash(resp.TxHash).Hex()) |
| 85 | + return fmt.Errorf("consumer contract execution failed: %s", resp.GetErrorMessage()) |
| 86 | +} |
| 87 | + |
| 88 | +logger.Info("Both transaction AND consumer contract execution succeeded", |
| 89 | + "txHash", common.BytesToHash(resp.TxHash).Hex()) |
| 90 | +``` |
| 91 | + |
| 92 | +**What this checks**: |
| 93 | + |
| 94 | +1. Transaction was mined successfully |
| 95 | +1. Your consumer contract's `onReport()` function executed without reverting |
| 96 | +1. Your business logic completed successfully |
| 97 | + |
| 98 | +## Common scenarios |
| 99 | + |
| 100 | +### Scenario 1: Everything succeeded |
| 101 | + |
| 102 | +```go |
| 103 | +// Transaction mined + Consumer contract executed successfully |
| 104 | +TxStatus: TX_STATUS_SUCCESS |
| 105 | +ReceiverContractExecutionStatus: RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS |
| 106 | +``` |
| 107 | + |
| 108 | +**What happened**: The report was delivered and your contract processed it successfully. |
| 109 | + |
| 110 | +**Action**: None needed - everything worked as expected. |
| 111 | + |
| 112 | +### Scenario 2: Transaction succeeded, but contract reverted |
| 113 | + |
| 114 | +```go |
| 115 | +// Transaction was mined, but your contract rejected the data |
| 116 | +TxStatus: TX_STATUS_SUCCESS |
| 117 | +ReceiverContractExecutionStatus: RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED |
| 118 | +``` |
| 119 | + |
| 120 | +**What happened**: The forwarder successfully submitted the transaction, but your consumer contract's `onReport()` function reverted during execution. |
| 121 | + |
| 122 | +**Common causes**: |
| 123 | + |
| 124 | +- Forwarder address mismatch: You configured the wrong forwarder address in your consumer contract (simulation forwarders are different from production forwarders - see [Supported Networks](/cre/guides/workflow/using-evm-client/supported-networks-go)) |
| 125 | +- Security check failed (if you configured expected values for workflow ID, owner, or name in your contract) |
| 126 | +- Custom validation in `_processReport()` rejected the data |
| 127 | +- ABI decoding failure (struct mismatch between workflow and contract) |
| 128 | +- Custom business logic constraints not met |
| 129 | + |
| 130 | +**Action**: Check the error message and review your consumer contract's validation logic. If moving from simulation to production, ensure you updated the forwarder address in your contract. |
| 131 | + |
| 132 | +### Scenario 3: Transaction failed |
| 133 | + |
| 134 | +```go |
| 135 | +// Transaction failed to be mined |
| 136 | +TxStatus: TX_STATUS_REVERTED or TX_STATUS_FATAL |
| 137 | +ReceiverContractExecutionStatus: N/A |
| 138 | +``` |
| 139 | + |
| 140 | +**What happened**: The transaction couldn't be mined on the blockchain. |
| 141 | + |
| 142 | +**Common causes**: |
| 143 | + |
| 144 | +- Insufficient gas |
| 145 | +- Network connectivity issues |
| 146 | +- Incorrect forwarder address |
| 147 | +- RPC endpoint problems |
| 148 | + |
| 149 | +**Action**: Check RPC endpoint, gas configuration, network status, and forwarder address. |
| 150 | + |
| 151 | +## Best practices helper function |
| 152 | + |
| 153 | +Create a reusable helper to verify both status levels: |
| 154 | + |
| 155 | +```go |
| 156 | +// verifyWriteSuccess checks both transaction and contract execution status |
| 157 | +func verifyWriteSuccess(resp *evm.WriteReportReply, logger *slog.Logger) error { |
| 158 | + // Check outer transaction |
| 159 | + if resp.TxStatus != evm.TxStatus_TX_STATUS_SUCCESS { |
| 160 | + return fmt.Errorf("transaction failed with status %v", resp.TxStatus) |
| 161 | + } |
| 162 | + |
| 163 | + // Check consumer contract execution |
| 164 | + if resp.ReceiverContractExecutionStatus != nil && |
| 165 | + *resp.ReceiverContractExecutionStatus == evm.ReceiverContractExecutionStatus_RECEIVER_CONTRACT_EXECUTION_STATUS_REVERTED { |
| 166 | + errorMsg := "unknown error" |
| 167 | + if resp.ErrorMessage != nil { |
| 168 | + errorMsg = *resp.ErrorMessage |
| 169 | + } |
| 170 | + return fmt.Errorf("consumer contract reverted: %s", errorMsg) |
| 171 | + } |
| 172 | + |
| 173 | + // Log success with transaction hash |
| 174 | + txHash := common.BytesToHash(resp.TxHash).Hex() |
| 175 | + logger.Info("Write verification succeeded", |
| 176 | + "txHash", txHash, |
| 177 | + "txStatus", resp.TxStatus, |
| 178 | + "contractStatus", resp.ReceiverContractExecutionStatus) |
| 179 | + |
| 180 | + return nil |
| 181 | +} |
| 182 | + |
| 183 | +// Usage in your workflow |
| 184 | +func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*MyResult, error) { |
| 185 | + logger := runtime.Logger() |
| 186 | + |
| 187 | + // ... prepare data and write report ... |
| 188 | + |
| 189 | + writePromise := contract.WriteReportFromMyData(runtime, data, nil) |
| 190 | + resp, err := writePromise.Await() |
| 191 | + if err != nil { |
| 192 | + return nil, fmt.Errorf("write report await failed: %w", err) |
| 193 | + } |
| 194 | + |
| 195 | + // Use the helper for complete verification |
| 196 | + if err := verifyWriteSuccess(resp, logger); err != nil { |
| 197 | + return nil, err |
| 198 | + } |
| 199 | + |
| 200 | + return &MyResult{TxHash: common.BytesToHash(resp.TxHash).Hex()}, nil |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +## Accessing error details |
| 205 | + |
| 206 | +The `WriteReportReply` provides multiple ways to access error information: |
| 207 | + |
| 208 | +```go |
| 209 | +resp, err := writePromise.Await() |
| 210 | +if err != nil { |
| 211 | + return fmt.Errorf("await failed: %w", err) |
| 212 | +} |
| 213 | + |
| 214 | +// Option 1: Direct field access (pointer, can be nil) |
| 215 | +if resp.ErrorMessage != nil { |
| 216 | + logger.Error("Error message (direct)", "message", *resp.ErrorMessage) |
| 217 | +} |
| 218 | + |
| 219 | +// Option 2: Using the getter method (safer, returns empty string if nil) |
| 220 | +logger.Info("Error message (getter)", "message", resp.GetErrorMessage()) |
| 221 | + |
| 222 | +// Option 3: Check status enum |
| 223 | +logger.Info("Contract execution status", "status", resp.GetReceiverContractExecutionStatus()) |
| 224 | +``` |
| 225 | + |
| 226 | +**Best practice**: Use the getter methods (`GetErrorMessage()`, `GetReceiverContractExecutionStatus()`) as they handle nil values safely. |
| 227 | + |
| 228 | +## Related resources |
| 229 | + |
| 230 | +- **[EVM Client Reference](/cre/reference/sdk/evm-client-go#evmwritereportreply)** - Complete API documentation for `WriteReportReply`, including all field definitions and status constant values |
| 231 | +- **[Building Consumer Contracts](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts)** - Learn about forwarder validation and the `IReceiver` interface |
| 232 | +- **[Supported Networks](/cre/guides/workflow/using-evm-client/supported-networks)** - Forwarder addresses for simulation and production |
| 233 | +- **[Submitting Reports Onchain](/cre/guides/workflow/using-evm-client/onchain-write/submitting-reports-onchain)** - Complete guide to the write process |
0 commit comments