Generating Contract Bindings
To interact with a smart contract from your Go workflow, you first need to create bindings. Bindings are type-safe Go interfaces auto-generated from your contract's ABI. They provide a bridge between your Go code and the EVM.
How they work depends on whether you are reading from or writing to the chain:
- For onchain reads, bindings provide Go functions that directly mirror your contract's
viewandpuremethods. - For onchain writes, bindings provide powerful helper methods to ABI-encode your data structures, preparing them to be sent in a report to a consumer contract.
This is a one-time code generation step performed using the CRE CLI.
The generation process
The CRE CLI provides an automated binding generator that reads contract ABIs and creates corresponding Go packages.
Step 1: Add your contract ABI
Place your contract's ABI JSON file into the contracts/evm/src/abi/ directory. For example, to generate bindings for a PriceUpdater contract, you would create contracts/evm/src/abi/PriceUpdater.abi with your ABI content.
Step 2: Generate the bindings
From your project root, run the binding generator:
cre generate-bindings evm
This command scans all .abi files in contracts/evm/src/abi/ and generates corresponding Go packages in contracts/evm/src/generated/. For each contract, two files are generated:
<ContractName>.go— The main binding for interacting with the contract<ContractName>_mock.go— A mock implementation for testing your workflows without deploying contracts
Using generated bindings
For onchain reads
For view or pure functions, the generator creates a client with methods that you can call directly. These methods return a Promise, which you must .Await() to get the result after consensus.
Example: A simple Storage contract
If you have a Storage.abi for a contract with a get() view function, you can use the bindings like this:
// Import the generated package for your contract, replacing "<project-name>" with your project's module name
import "<project-name>/contracts/evm/src/generated/storage"
import "github.com/ethereum/go-ethereum/common"
// In your workflow function...
evmClient := &evm.Client{ ChainSelector: config.ChainSelector }
contractAddress := common.HexToAddress(config.ContractAddress)
// Create a new contract instance
storageContract, err := storage.NewStorage(evmClient, contractAddress, nil)
if err != nil { /* ... */ }
// Call a read-only method - note that it returns the decoded type directly
value, err := storageContract.Get(runtime, big.NewInt(-3)).Await() // -3 = finalized block
if err != nil { /* ... */ }
// value is already a *big.Int, ready to use!
For onchain writes
For onchain writes, your goal is to send an ABI-encoded report to your consumer contract. The binding generator creates helper methods that handle the entire process: creating the report, sending it for consensus, and delivering it to the chain.
Signaling the generator
To generate the necessary Go types and write helpers, your ABI must include at least one public or external function that uses the data struct you want to send as a parameter.
The generated helper method is named after the input struct type. For example, a struct named PriceData will generate a WriteReportFromPriceData helper.
Example: A PriceUpdater contract ABI
This ABI contains a PriceData struct and a public updatePrices function. This is all the generator needs.
// contracts/evm/src/PriceUpdater.sol
// This contract can be used purely to generate the bindings.
// The actual onchain logic can live elsewhere.
contract PriceUpdater {
struct PriceData {
uint256 ethPrice;
uint256 btcPrice;
}
// The struct type (`PriceData`) determines the generated helper name.
// The generator will create a `WriteReportFromPriceData` method.
function updatePrices(PriceData memory) public {}
}
Using write bindings in a workflow
After running cre generate-bindings, you can use the generated PriceUpdater client to send a report. The workflow code will look like this:
// Import the generated package for your contract, replacing "<project-name>" with your project's module name
import "<project-name>/contracts/evm/src/generated/price_updater"
import "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
import "github.com/ethereum/go-ethereum/common"
import "math/big"
import "fmt"
// In your workflow function...
// The address should be your PROXY contract's address.
contractAddress := common.HexToAddress(config.ProxyAddress)
evmClient := &evm.Client{ ChainSelector: config.ChainSelector }
// 1. Create a new contract instance using the generated bindings.
// Even though it's called `price_updater`, it's configured with your proxy address.
priceUpdater, err := price_updater.NewPriceUpdater(evmClient, contractAddress, nil)
if err != nil { /* ... */ }
// 2. Instantiate the generated Go struct with your data.
reportData := price_updater.PriceData{
EthPrice: big.NewInt(4000_000000),
BtcPrice: big.NewInt(60000_000000),
}
// 3. Call the generated WriteReportFrom<StructName> method on the contract instance.
// This method name is derived from the input struct of your contract's function.
writePromise := priceUpdater.WriteReportFromPriceData(runtime, reportData, nil)
// 4. Await the promise to confirm the transaction has been mined.
resp, err := writePromise.Await()
if err != nil {
return nil, fmt.Errorf("WriteReport await failed: %w", err)
}
// 5. The response contains the transaction hash.
logger := runtime.Logger()
logger.Info("Write report transaction succeeded", "txHash", common.BytesToHash(resp.TxHash).Hex())
For event logs
The binding generator also creates powerful helpers for interacting with your contract's events. You can easily trigger a workflow when an event is emitted and decode the event data into a type-safe Go struct.
Example: A contract with a UserAdded event
contract UserDirectory {
event UserAdded(address indexed userAddress, string userName);
function addUser(string calldata userName) external {
emit UserAdded(msg.sender, userName);
}
}
Triggering and Decoding Events
After generating bindings for the UserDirectory ABI, you can use the helpers to create a trigger and decode the logs in your handler.
import (
"log/slog"
"<project-name>/contracts/evm/src/generated/user_directory" // Replace "<project-name>" with your project's module name
"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
"github.com/smartcontractkit/cre-sdk-go/cre"
)
// In InitWorkflow, create an instance of the contract binding and use it
// to generate a trigger for the "UserAdded" event.
func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
// ...
userDirectory, err := user_directory.NewUserDirectory(evmClient, contractAddress, nil)
if err != nil { /* ... */ }
// Use the generated helper to create a trigger for the UserAdded event.
// Set confidence to evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED to only trigger on finalized blocks.
// The last argument (filters) is nil to listen for all UserAdded events.
userAddedTrigger, err := userDirectory.LogTriggerUserAddedLog(chainSelector, evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED, nil)
if err != nil { /* ... */ }
return cre.Workflow[*Config]{
cre.Handler(
userAddedTrigger,
onUserAdded,
),
}, nil
}
// The handler function receives the raw event log.
func onUserAdded(config *Config, runtime cre.Runtime, log *evm.Log) (string, error) {
logger := runtime.Logger()
// You must re-create the contract instance to access the decoder.
userDirectory, err := user_directory.NewUserDirectory(evmClient, contractAddress, nil)
if err != nil { /* ... */ }
// Use the generated Codec to decode the raw log into a typed Go struct.
decodedLog, err := userDirectory.Codec.DecodeUserAdded(log)
if err != nil {
return "", fmt.Errorf("failed to decode log: %w", err)
}
logger.Info("New user added!", "address", decodedLog.UserAddress, "name", decodedLog.UserName)
return "ok", nil
}
What the CLI Generates
The generator creates a Go package for each ABI file.
- For all contracts:
Codecinterface for low-level encoding and decoding.
- For onchain reads:
- A contract client struct (e.g.,
Storage) to interact with. - A constructor function (e.g.,
NewStorage(...)) to instantiate the client. - Method wrappers for each
view/purefunction (e.g.,storage.Get(...)) that return a promise.
- A contract client struct (e.g.,
- For onchain writes:
- A Go type for each
structexposed via a public function (e.g.,price_updater.PriceData). - A
WriteReportFrom<StructName>method on the contract client struct (e.g.,priceUpdater.WriteReportFromPriceData(...)). This method handles the full process of generating and sending a report and returns a promise that resolves with the transaction details.
- A Go type for each
- For events:
- A Go struct for each
event(e.g.,UserAdded). - A
Decode<EventName>method on theCodecto parse raw log data into the corresponding Go struct. - A
LogTrigger<EventName>Logmethod on the contract client to easily create a workflow trigger. - A
FilterLogs<EventName>method to query historical logs for that event.
- A Go struct for each
Using mock bindings for testing
The <ContractName>_mock.go files allow you to test your workflows without deploying or interacting with real contracts. Each mock struct provides:
- Test-friendly constructor:
New<ContractName>Mock(address, evmMockClient)creates a mock instance - Mockable methods: Set custom function implementations for each contract
view/purefunction - Type safety: The same input/output types as the real binding
Complete example: Testing a workflow with mocks
Let's say you have a workflow in my-workflow/main.go that reads from a Storage contract. Create a test file named main_test.go in the same directory.
// File: my-workflow/main_test.go
package main
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
evmmock "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock"
"github.com/stretchr/testify/require"
"your-project/contracts/evm/src/generated/storage"
)
// Define your config types in the test file to match your workflow's structure
// Note: Your main.go likely has //go:build wasip1 (for WASM compilation),
// which means those types aren't available when running regular Go tests.
// So you need to redefine them here in your test file.
type EvmConfig struct {
StorageAddress string `json:"storageAddress"`
ChainName string `json:"chainName"`
}
type Config struct {
Evms []EvmConfig `json:"evms"`
}
func TestStorageRead(t *testing.T) {
// 1. Set up your config
config := &Config{
Evms: []EvmConfig{
{
StorageAddress: "0xa17CF997C28FF154eDBae1422e6a50BeF23927F4",
ChainName: "ethereum-testnet-sepolia",
},
},
}
// 2. Create a mock EVM client
chainSelector := uint64(evm.EthereumTestnetSepolia)
evmMock, err := evmmock.NewClientCapability(chainSelector, t)
require.NoError(t, err)
// 3. Create a mock Storage contract and set up mock behavior
storageAddress := common.HexToAddress(config.Evms[0].StorageAddress)
storageMock := storage.NewStorageMock(storageAddress, evmMock)
// 4. Mock the Get() function to return a controlled value
storageMock.Get = func() (*big.Int, error) {
return big.NewInt(42), nil
}
// 5. Now when your workflow code creates a Storage contract with this evmMock,
// it will automatically use the mocked Get() function.
// The mock is registered with the evmMock, so any contract at this address
// will use the mock behavior you defined.
// In a real test, you would call your workflow function here and verify results.
// Example:
// result, err := onCronTrigger(config, runtime, &cron.Payload{})
// require.NoError(t, err)
// require.Equal(t, big.NewInt(42), result.StorageValue)
// For this demo, we just verify the mock was set up
require.NotNil(t, storageMock)
t.Logf("Mock set up successfully - Get() will return 42")
}
Running your tests
From your project root, run:
# Test a specific workflow
go test ./my-workflow
# Test with verbose output (shows t.Logf messages)
go test -v ./my-workflow
# Test all workflows in your project
go test ./...
Expected output with -v flag:
=== RUN TestStorageRead
main_test.go:55: Mock Storage contract set up at 0xa17CF997C28FF154eDBae1422e6a50BeF23927F4
main_test.go:56: When Storage.Get() is called, it will return: 42
--- PASS: TestStorageRead (0.00s)
PASS
ok onchain-calculator/my-calculator-workflow 0.257s
The test passes, confirming your mock contract is set up correctly. In a real workflow test, you would call your workflow function and verify it produces the expected results using the mocked contract.
Best practices for workflow testing
- Name test files correctly: Use
<name>_test.go(e.g.,main_test.go) and place them in your workflow directory - Test function naming: Start test functions with
Test(e.g.,TestMyWorkflow,TestCronTrigger) - Mock all external dependencies: Use mock contracts for EVM calls and mock HTTP clients for API requests
- Test different scenarios: Create separate test functions for success cases, error cases, and edge cases
Complete reference example
For a comprehensive example showing how to test workflows with multiple triggers (cron, HTTP, EVM log) and multiple mock contracts, see the Custom Data Feed demo workflow's workflow_test.go file.
To generate this example:
- Run
cre initfrom your project directory - Select Golang as your language
- Choose the "Custom data feed: Updating on-chain data periodically using offchain API data" template
- After initialization completes, examine the generated
workflow_test.gofile in your workflow directory
This generated test file demonstrates real-world patterns for testing complex workflows with multiple capabilities and mock contracts.
Best practices
- Regenerate when needed: Re-run the generator if you update your contract ABIs.
- Handle errors: Always check for errors at each step.
- Organize ABIs: Keep your ABI files clearly named in the
contracts/evm/src/abi/directory. - Use mocks in tests: Leverage the generated mock bindings to test your workflows in isolation without needing deployed contracts.
Where to go next
Now that you know how to generate bindings, you can use them to read data from or write data to your contracts, or trigger workflows from events.