Using WriteReportFrom Helpers
This guide explains how to write data to a smart contract using the WriteReportFrom<StructName>() helper methods that are automatically generated from your contract's ABI. This is the recommended and simplest approach for most users.
Use this approach when:
- You're sending a struct to your consumer contract
- The struct appears in a
publicorexternalfunction's signature (as a parameter or return value); this is required for the binding generator to detect it in your contract's ABI and create the helper method
Don't meet these requirements? See the Onchain Write page to find the right approach for your scenario.
Prerequisites
Before you begin, ensure you have:
- A consumer contract deployed that implements the
IReceiverinterface- See Building Consumer Contracts if you need to create one
- Generated bindings from your consumer contract's ABI
- See Generating Bindings if you haven't created them yet
What the helper does for you
The WriteReportFrom<StructName>() helper method automates the entire onchain write process:
- ABI-encodes your struct into bytes
- Generates a cryptographically signed report via
runtime.GenerateReport() - Submits the report to the blockchain via
evm.Client.WriteReport() - Returns a promise with the transaction details
All of this happens in a single method call, making your workflow code clean and simple.
The write pattern
Writing to contracts using binding helpers follows this simple pattern:
- Create an EVM client with your target chain selector
- Instantiate the contract binding with the consumer contract's address
- Prepare your data using the generated struct type
- Call the write helper and await the result
Let's walk through each step with a complete example.
Step-by-step example
Assume you have a consumer contract with a struct that looks like this:
struct UpdateReserves {
uint256 totalMinted;
uint256 totalReserve;
}
// This function makes the struct appear in the ABI
function processReserveUpdate(UpdateReserves memory update) public {
// ... logic
}
Step 1: Create an EVM client
First, create an EVM client configured for the chain where your consumer contract is deployed:
import (
"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
)
func updateReserves(config *Config, runtime cre.Runtime, evmConfig EvmConfig, totalSupply *big.Int, totalReserveScaled *big.Int) error {
logger := runtime.Logger()
// Create EVM client with your target chain
evmClient := &evm.Client{
ChainSelector: evmConfig.ChainSelector, // e.g., 16015286601757825753 for Sepolia
}
Step 2: Instantiate the contract binding
Create an instance of your generated binding, pointing it at your consumer contract's address:
import (
"contracts/evm/src/generated/reserve_manager"
"github.com/ethereum/go-ethereum/common"
)
// Convert the address string from your config to common.Address
contractAddress := common.HexToAddress(evmConfig.ConsumerAddress)
// Create the binding instance
reserveManager, err := reserve_manager.NewReserveManager(evmClient, contractAddress, nil)
if err != nil {
return fmt.Errorf("failed to create reserve manager: %w", err)
}
Step 3: Prepare your data
Create an instance of the generated struct type with your data:
// Use the generated struct type from your bindings
updateData := reserve_manager.UpdateReserves{
TotalMinted: totalSupply, // *big.Int
TotalReserve: totalReserveScaled, // *big.Int
}
logger.Info("Prepared data for onchain write",
"totalMinted", totalSupply.String(),
"totalReserve", totalReserveScaled.String())
Step 4: Call the write helper and await
Call the generated WriteReportFrom<StructName>() method and await the result:
// Call the generated helper - it handles encoding, report generation, and submission
writePromise := reserveManager.WriteReportFromUpdateReserves(runtime, updateData, nil)
logger.Info("Waiting for write report response")
// Await the transaction result
resp, err := writePromise.Await()
if err != nil {
logger.Error("WriteReport failed", "error", err)
return fmt.Errorf("failed to write report: %w", err)
}
// Log the successful transaction
txHash := common.BytesToHash(resp.TxHash).Hex()
logger.Info("Write report transaction succeeded", "txHash", txHash)
return nil
}
Understanding the response
The write helper returns an evm.WriteReportReply struct with comprehensive transaction details:
type WriteReportReply struct {
TxStatus TxStatus // SUCCESS, REVERTED, or FATAL
ReceiverContractExecutionStatus *ReceiverContractExecutionStatus // Contract execution status
TxHash []byte // Transaction hash
TransactionFee *pb.BigInt // Fee paid in Wei
ErrorMessage *string // Error message if failed
}
Key fields to check:
TxStatus: Indicates whether the transaction succeeded, reverted, or had a fatal errorTxHash: The transaction hash you can use to verify on a block explorer (e.g., Etherscan)TransactionFee: The total gas cost paid for the transaction in WeiReceiverContractExecutionStatus: Whether your consumer contract'sonReport()function executed successfullyErrorMessage: If the transaction failed, this field contains details about what went wrong
Complete example
Here's a complete, runnable workflow function that demonstrates the end-to-end pattern:
package main
import (
"contracts/evm/src/generated/reserve_manager"
"fmt"
"log/slog"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
"github.com/smartcontractkit/cre-sdk-go/cre"
)
type EvmConfig struct {
ConsumerAddress string `json:"consumerAddress"`
ChainSelector uint64 `json:"chainSelector"`
}
type Config struct {
// Add other config fields from your workflow here
}
func updateReserves(config *Config, runtime cre.Runtime, evmConfig EvmConfig, totalSupply *big.Int, totalReserveScaled *big.Int) error {
logger := runtime.Logger()
logger.Info("Updating reserves", "totalSupply", totalSupply, "totalReserveScaled", totalReserveScaled)
// Create EVM client with chain selector
evmClient := &evm.Client{
ChainSelector: evmConfig.ChainSelector,
}
// Create contract binding
contractAddress := common.HexToAddress(evmConfig.ConsumerAddress)
reserveManager, err := reserve_manager.NewReserveManager(evmClient, contractAddress, nil)
if err != nil {
return fmt.Errorf("failed to create reserve manager: %w", err)
}
logger.Info("Writing report", "totalSupply", totalSupply, "totalReserveScaled", totalReserveScaled)
// Call the write method
writePromise := reserveManager.WriteReportFromUpdateReserves(runtime, reserve_manager.UpdateReserves{
TotalMinted: totalSupply,
TotalReserve: totalReserveScaled,
}, nil)
logger.Info("Waiting for write report response")
// Await the transaction
resp, err := writePromise.Await()
if err != nil {
logger.Error("WriteReport await failed", "error", err, "errorType", fmt.Sprintf("%T", err))
return fmt.Errorf("failed to write report: %w", err)
}
logger.Info("Write report transaction succeeded", "txHash", common.BytesToHash(resp.TxHash).Hex())
return nil
}
// NOTE: This is a placeholder. You would need a full workflow with InitWorkflow,
// a trigger, and a callback that calls this `updateReserves` function.
func main() {
// wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
}
Configuring gas limits
By default, the SDK automatically estimates gas limits for your transactions. However, for complex transactions or to ensure sufficient gas, you can explicitly set a gas limit:
// Create a gas configuration
gasConfig := &evm.GasConfig{
GasLimit: 1000000, // Adjust based on your contract's needs
}
// Pass it as the third argument to the write helper
writePromise := reserveManager.WriteReportFromUpdateReserves(runtime, updateData, gasConfig)
Best practices
- Always check errors: Both the write call and the
.Await()can fail—handle both error paths - Log transaction details: Include transaction hashes in your logs for debugging and monitoring
- Validate response status: Check the
TxStatusfield to ensure the transaction succeeded - Override gas limits when needed: For complex transactions, set explicit gas limits higher than the automatic estimates to avoid "out of gas" errors
- Monitor contract execution: Check
ReceiverContractExecutionStatusto ensure your consumer contract processed the data correctly
Troubleshooting
Transaction failed with "out of gas"
- Increase the
GasLimitin yourGasConfig - Check if your consumer contract's logic is more complex than expected
"WriteReport await failed" error
- Check that your consumer contract address is correct
- Verify you're using the correct chain selector
- Ensure your account has sufficient funds for gas
Transaction succeeded but contract didn't update
- Check the
ReceiverContractExecutionStatusfield - Review your consumer contract's
onReport()logic for validation failures - Verify the struct fields match what your contract expects
Learn more
- Onchain Write Overview: Understand all onchain write approaches
- Building Consumer Contracts: Create secure consumer contracts
- Generating Bindings: Generate type-safe contract bindings
- EVM Client Reference: Complete API documentation