diff --git a/building-blocks/indexer-block-trigger/README.md b/building-blocks/indexer-block-trigger/README.md new file mode 100644 index 00000000..b693fa52 --- /dev/null +++ b/building-blocks/indexer-block-trigger/README.md @@ -0,0 +1,174 @@ +# CRE Indexer Block Trigger Workflows + +Workflows for processing new blocks and transactions using block-triggered webhooks (from Alchemy Notify) and matching +against watched addresses. These workflows demonstrate the **block trigger pattern** where the workflow reacts to +incoming block data and extracts relevant transactions. + +## Directory Structure + +``` +building-blocks/indexer-block-trigger/ +├── block-trigger-go/ (Go-based workflow) +│ └── workflow/ +│ ├── main.go +│ ├── config.staging.json +│ ├── config.production.json +│ ├── workflow.yaml +│ └── README.md +├── block-trigger-ts/ (TypeScript-based workflow) + └── workflow/ + ├── main.ts + ├── package.json + ├── config.staging.json (optional) + ├── workflow.yaml + └── README.md + +``` + +## Overview + +These workflows demonstrate how to: +- React to block events via HTTP webhook triggers +(We use Alchemy Notify for this workflow) +- Match transactions to a list of watched addresses +- Process and return JSON-formatted block and transaction data +- Implement the same logic in both Go and TypeScript + +Both workflows process incoming block data and extract: +- Block number, hash, timestamp +- All transactions in the block +- Transactions where the `to` address matches a watched address + +## Workflows + +### 1. block-trigger-go (Go Implementation) + +**Language:** Go + +**Features:** +- Uses `http.Trigger` from CRE Go SDK +- Matches transactions to watched addresses from config +- Returns formatted JSON summary of block and matched transactions + +**Running the workflow:** +```bash +cd building-blocks/indexer-block-trigger/block-trigger-go +cre workflow simulate workflow --non-interactive --trigger-index 0 --http-payload test-block.json --target staging-settings +``` + +### 2. block-trigger-ts (TypeScript Implementation) + +**Language:** TypeScript + +**Features:** +- Uses HTTP trigger from CRE TypeScript SDK +- Matches transactions to watched addresses from config +- Returns formatted JSON summary of block and matched transactions + +**Running the workflow:** +```bash +cd building-blocks/indexer-block-trigger/block-trigger-ts/workflow +bun install +cre workflow simulate workflow --non-interactive --trigger-index 0 --http-payload test-block.json --target staging-settings +``` + +## Setup and Testing + +### Prerequisites + +**For Go workflow:** +1. Install CRE CLI +2. Login: `cre login` +3. Install Go + +**For TypeScript workflow:** +1. Install CRE CLI +2. Login: `cre login` +3. Install Bun (or Node.js) +4. Run `bun install` in the workflow directory + +### Running the Workflows + +**Go Workflow:** +```bash +cd building-blocks/indexer-block-trigger/block-trigger-go +cre workflow simulate workflow --non-interactive --trigger-index 0 --http-payload test-block.json --target staging-settings +``` + +**TypeScript Workflow:** +```bash +cd building-blocks/indexer-block-trigger/block-trigger-ts +cre workflow simulate workflow --non-interactive --trigger-index 0 --http-payload test-block.json --target staging-settings +``` + +### Example Output + +Both workflows return JSON output like: + +```json +{ + "blockNumber": 12345678, + "blockHash": "0xabc...", + "timestamp": 1700000000, + "totalLogs": 42, + "uniqueTransactions": 10, + "matchedTransactions": 2, + "transactions": [ + { + "hash": "0xdef...", + "from": "0x...", + "to": "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "value": "1000000000000000000", + ... + } + ] +} +``` + +## Setting Up Alchemy Webhooks + +To use Alchemy for block-triggered workflows, follow these steps: + +1. Sign up on Alchemy and navigate to their dashboard. +2. Create a new app on the dashboard with your preferred network. +3. Click on your app to open the app dashboard and scroll down to the `services` section. +4. Click on the Webhooks service and in the pane that opens, click on `Real-time Notifications`, then click on `Get Started`. +5. Choose webhook type `Custom` to listen for new blocks or events on every new block. +6. In the custom webhook pane, add other details including webhook name, chain, network, query template, and webhook URL. +7. Click on `Create Webhook` to save the webhook and test the webhook URL. + +**Tips:** +- Make sure your webhook URL is accessible and correctly configured to receive POST requests. +- You may want to use a tool like [Webhook.site](https://webhook.site/) for initial testing. +- Double-check the network and chain settings to match your workflow requirements. +- The query template should match the data you want to extract from each block/event. + +**Example Alchemy Webhook Config:** + +![Example Alchemy Webhook Config](https://github.com/user-attachments/assets/80a73519-08b9-4f16-8345-e491c38bf6af) + +## Example Use Cases + +### 1. Monitoring High-Value Addresses +Track transactions to specific addresses in real time: +```json +{ + "watchedAddresses": ["0x...", "0x..."] +} +``` + +### 2. Contract Interaction Tracking +Detect when contracts of interest receive transactions: +```json +{ + "watchedAddresses": ["0xContract1", "0xContract2"] +} +``` + +### 3. Block-Level Analytics +Summarize block activity and matched transactions for analytics dashboards. + +## Reference Documentation + +- [CRE Documentation](https://docs.chain.link/cre) +- [Alchemy Webhooks](https://www.alchemy.com/docs/reference/custom-webhook) diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/.gitignore b/building-blocks/indexer-block-trigger/block-trigger-go/.gitignore new file mode 100644 index 00000000..03bd4129 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/go.mod b/building-blocks/indexer-block-trigger/block-trigger-go/go.mod new file mode 100644 index 00000000..4908cb12 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/go.mod @@ -0,0 +1,19 @@ +module block-trigger-go + +go 1.25.3 + +require ( + github.com/smartcontractkit/cre-sdk-go v1.0.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2776a35 // indirect + github.com/stretchr/testify v1.11.1 // indirect + google.golang.org/protobuf v1.36.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/go.sum b/building-blocks/indexer-block-trigger/block-trigger-go/go.sum new file mode 100644 index 00000000..61e4e765 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/go.sum @@ -0,0 +1,24 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2776a35 h1:hhKdzgNZT+TnohlmJODtaxlSk+jyEO79YNe8zLFtp78= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2776a35/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/cre-sdk-go v1.0.0 h1:O52/QDmw/W8SJ7HQ9ASlVx7alSMGsewjL0Y8WZmgf5w= +github.com/smartcontractkit/cre-sdk-go v1.0.0/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0 h1:nP6PVWrrTIICvjwQuFitsQecQWbqpPaYzaTEjx92eTQ= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0/go.mod h1:M83m3FsM1uqVu06OO58mKUSZJjjH8OGJsmvFpFlRDxI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/project.yaml b/building-blocks/indexer-block-trigger/block-trigger-go/project.yaml new file mode 100644 index 00000000..81012cc9 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/project.yaml @@ -0,0 +1,27 @@ +# ========================================================================== +# CRE PROJECT SETTINGS FILE +# ========================================================================== +# Project-specific settings for CRE CLI targets. +# Each target defines cre-cli, account, and rpcs groups. +# +# Example custom target: +# my-target: +# account: +# workflow-owner-address: "0x123..." # Optional: Owner wallet/MSIG address (used for --unsigned transactions) +# rpcs: +# - chain-name: ethereum-mainnet # Required: Chain RPC endpoints +# url: "https://mainnet.infura.io/v3/KEY" + +# ========================================================================== +staging-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + +# ========================================================================== +production-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + - chain-name: ethereum-mainnet + url: https://mainnet.infura.io/v3/ diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/secrets.yaml b/building-blocks/indexer-block-trigger/block-trigger-go/secrets.yaml new file mode 100644 index 00000000..7b85d864 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/secrets.yaml @@ -0,0 +1 @@ +secretsNames: diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/workflow/README.md b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/README.md new file mode 100644 index 00000000..4e5b52d3 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/README.md @@ -0,0 +1,48 @@ +# CRE Indexer Block Trigger Workflow (Go) + +This workflow processes new blocks and transactions using block-triggered webhooks (e.g., Alchemy Notify) and matches against watched addresses. It demonstrates the **block trigger pattern** in Go. + +## Features +- Uses `http.Trigger` from CRE Go SDK +- Matches transactions to watched addresses from config +- Returns formatted JSON summary of block and matched transactions + +## Setup and Prerequisites +1. Install CRE CLI +2. Login: `cre login` +3. Install Go + +## Running the Workflow +```bash +cd building-blocks/indexer-block-trigger/block-trigger-go +cre workflow simulate workflow --non-interactive --trigger-index 0 --http-payload test-block.json --target staging-settings +``` + +## Example Output +```json +{ + "blockNumber": 12345678, + "blockHash": "0xabc...", + "timestamp": 1700000000, + "totalLogs": 42, + "uniqueTransactions": 10, + "matchedTransactions": 2, + "transactions": [ + { + "hash": "0xdef...", + "from": "0x...", + "to": "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "value": "1000000000000000000" + } + ] +} +``` + +## Example Use Cases +- Monitoring high-value addresses +- Contract interaction tracking +- Block-level analytics + +## Reference Documentation +- [CRE Documentation](https://docs.chain.link/cre) +- [Alchemy Webhooks](https://www.alchemy.com/docs/reference/custom-webhook) diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.production.json b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.production.json new file mode 100644 index 00000000..612d1319 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.production.json @@ -0,0 +1,6 @@ +{ + "watchedAddresses": [ + "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "0x6edce65403992e310a62460808c4b910d972f10f" + ] +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.staging.json b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.staging.json new file mode 100644 index 00000000..612d1319 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.staging.json @@ -0,0 +1,6 @@ +{ + "watchedAddresses": [ + "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "0x6edce65403992e310a62460808c4b910d972f10f" + ] +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/workflow/main.go b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/main.go new file mode 100644 index 00000000..5d0f9a29 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/main.go @@ -0,0 +1,215 @@ +//go:build wasip1 + +package main + +import ( + "encoding/json" + "fmt" + "log/slog" + "strings" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" +) + +// Workflow configuration loaded from the config.json file +type Config struct { + WatchedAddresses []string `json:"watchedAddresses"` +} + +// AlchemyWebhookPayload represents the webhook data from Alchemy +type AlchemyWebhookPayload struct { + WebhookID string `json:"webhookId"` + ID string `json:"id"` + CreatedAt string `json:"createdAt"` + Type string `json:"type"` + Event struct { + Data struct { + Block struct { + Hash string `json:"hash"` + Number int64 `json:"number"` + Timestamp int64 `json:"timestamp"` + Logs []struct { + Data string `json:"data"` + Topics []string `json:"topics"` + Index int `json:"index"` + Account struct { + Address string `json:"address"` + } `json:"account"` + Transaction struct { + Hash string `json:"hash"` + Nonce int `json:"nonce"` + Index int `json:"index"` + From Address `json:"from"` + To *Address `json:"to"` + Value string `json:"value"` + GasPrice string `json:"gasPrice"` + MaxFeePerGas *string `json:"maxFeePerGas"` + MaxPriorityFeePerGas *string `json:"maxPriorityFeePerGas"` + Gas int `json:"gas"` + Status int `json:"status"` + GasUsed int `json:"gasUsed"` + CumulativeGasUsed int `json:"cumulativeGasUsed"` + EffectiveGasPrice string `json:"effectiveGasPrice"` + CreatedContract *Address `json:"createdContract"` + } `json:"transaction"` + } `json:"logs"` + } `json:"block"` + } `json:"data"` + } `json:"event"` +} + +type Address struct { + Address string `json:"address"` +} + +// Transaction represents a processed transaction +type Transaction struct { + Hash string `json:"hash"` + Nonce int `json:"nonce"` + Index int `json:"index"` + From string `json:"from"` + To *string `json:"to"` + Value string `json:"value"` + GasPrice string `json:"gasPrice"` + Gas int `json:"gas"` + Status int `json:"status"` + GasUsed int `json:"gasUsed"` + BlockNumber int64 `json:"blockNumber"` + BlockHash string `json:"blockHash"` + Timestamp int64 `json:"timestamp"` +} + +// ExecutionResult represents the workflow output +type ExecutionResult struct { + BlockNumber int64 `json:"blockNumber"` + BlockHash string `json:"blockHash"` + Timestamp int64 `json:"timestamp"` + TotalLogs int `json:"totalLogs"` + UniqueTransactions int `json:"uniqueTransactions"` + MatchedTransactions int `json:"matchedTransactions"` + Transactions []Transaction `json:"transactions"` +} + +// TransactionStore is a simple in-memory database mock +type TransactionStore struct { + watchedAddresses map[string]bool + transactions map[string]Transaction +} + +func createTransactionStore(addresses []string) *TransactionStore { + store := &TransactionStore{ + watchedAddresses: make(map[string]bool), + transactions: make(map[string]Transaction), + } + + for _, addr := range addresses { + store.watchedAddresses[strings.ToLower(addr)] = true + } + + return store +} + +func (s *TransactionStore) isWatchedAddress(address *string) bool { + if address == nil { + return false + } + return s.watchedAddresses[strings.ToLower(*address)] +} + +func (s *TransactionStore) saveTransaction(tx Transaction) { + s.transactions[tx.Hash] = tx +} + +// Workflow implementation with HTTP trigger +func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) { + httpTrigger := http.Trigger(&http.Config{}) + + return cre.Workflow[*Config]{ + cre.Handler(httpTrigger, onHttpTrigger), + }, nil +} + +func onHttpTrigger(config *Config, runtime cre.Runtime, payload *http.Payload) (*ExecutionResult, error) { + logger := runtime.Logger() + + // Parse the webhook payload + var blockData AlchemyWebhookPayload + if err := json.Unmarshal(payload.Input, &blockData); err != nil { + logger.Error("Failed to parse webhook payload", "error", err) + return nil, fmt.Errorf("invalid webhook payload: %w", err) + } + + block := blockData.Event.Data.Block + logger.Info("Processing block", "blockNumber", block.Number, "hash", block.Hash) + + // Initialize store with watched addresses from config + store := createTransactionStore(config.WatchedAddresses) + + // Extract unique transactions from logs + processedHashes := make(map[string]bool) + var matchedTransactions []Transaction + + for _, log := range block.Logs { + tx := log.Transaction + + // Skip if we've already processed this transaction + if processedHashes[tx.Hash] { + continue + } + processedHashes[tx.Hash] = true + + // Check if the 'to' address matches any watched addresses + var toAddress *string + if tx.To != nil { + toAddress = &tx.To.Address + } + + if store.isWatchedAddress(toAddress) { + logger.Info("Match found!", "txHash", tx.Hash, "toAddress", *toAddress) + + // Create transaction record + transaction := Transaction{ + Hash: tx.Hash, + Nonce: tx.Nonce, + Index: tx.Index, + From: tx.From.Address, + To: toAddress, + Value: tx.Value, + GasPrice: tx.GasPrice, + Gas: tx.Gas, + Status: tx.Status, + GasUsed: tx.GasUsed, + BlockNumber: block.Number, + BlockHash: block.Hash, + Timestamp: block.Timestamp, + } + + // Save to store + store.saveTransaction(transaction) + matchedTransactions = append(matchedTransactions, transaction) + } + } + + logger.Info("Block processing complete", + "block", block.Number, + "totalLogs", len(block.Logs), + "uniqueTransactions", len(processedHashes), + "matchedTransactions", len(matchedTransactions)) + + // Return summary + return &ExecutionResult{ + BlockNumber: block.Number, + BlockHash: block.Hash, + Timestamp: block.Timestamp, + TotalLogs: len(block.Logs), + UniqueTransactions: len(processedHashes), + MatchedTransactions: len(matchedTransactions), + Transactions: matchedTransactions, + }, nil +} + +func main() { + wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow) +} \ No newline at end of file diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/workflow/test-block.json b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/test-block.json new file mode 100644 index 00000000..2ea91215 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/test-block.json @@ -0,0 +1,46 @@ +{ + "webhookId": "wh_test", + "id": "whevt_test", + "createdAt": "2025-11-25T16:07:15.266Z", + "type": "GRAPHQL", + "event": { + "data": { + "block": { + "hash": "0xf49c1958f057aae44d4636511744fd5f66e0a1572c68c3b6f89c6460b87b3d08", + "number": 9704813, + "timestamp": 1764086832, + "logs": [ + { + "data": "0x", + "topics": ["0xtest"], + "index": 0, + "account": { + "address": "0x4c4df78f0af62846259c6a5678498f6f2a9012b9" + }, + "transaction": { + "hash": "0xb3d49123ead828e1c7d4830c7c08b09e671a2ada491a57613dc95a6d9db377bc", + "nonce": 8617, + "index": 3, + "from": { + "address": "0xc5d959a56de33f79f318c806ca069652769d7e75" + }, + "to": { + "address": "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc" + }, + "value": "0x0", + "gasPrice": "0x7735940c", + "maxFeePerGas": "0x2540be400", + "maxPriorityFeePerGas": "0x77359400", + "gas": 15000000, + "status": 1, + "gasUsed": 116097, + "cumulativeGasUsed": 589096, + "effectiveGasPrice": "0x7735940c", + "createdContract": null + } + } + ] + } + } + } +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-go/workflow/workflow.yaml b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/workflow.yaml new file mode 100644 index 00000000..eb7e686b --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/workflow.yaml @@ -0,0 +1,34 @@ +# ========================================================================== +# CRE WORKFLOW SETTINGS FILE +# ========================================================================== +# Workflow-specific settings for CRE CLI targets. +# Each target defines user-workflow and workflow-artifacts groups. +# Settings here override CRE Project Settings File values. +# +# Example custom target: +# my-target: +# user-workflow: +# workflow-name: "MyExampleWorkflow" # Required: Workflow Registry name +# workflow-artifacts: +# workflow-path: "./main.ts" # Path to workflow entry point +# config-path: "./config.yaml" # Path to config file +# secrets-path: "../secrets.yaml" # Path to secrets file (project root by default) + +# ========================================================================== +staging-settings: + user-workflow: + workflow-name: "workflow-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" + secrets-path: "" + + +# ========================================================================== +production-settings: + user-workflow: + workflow-name: "workflow-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" + secrets-path: "" \ No newline at end of file diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/.gitignore b/building-blocks/indexer-block-trigger/block-trigger-ts/.gitignore new file mode 100644 index 00000000..03bd4129 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/project.yaml b/building-blocks/indexer-block-trigger/block-trigger-ts/project.yaml new file mode 100644 index 00000000..81012cc9 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/project.yaml @@ -0,0 +1,27 @@ +# ========================================================================== +# CRE PROJECT SETTINGS FILE +# ========================================================================== +# Project-specific settings for CRE CLI targets. +# Each target defines cre-cli, account, and rpcs groups. +# +# Example custom target: +# my-target: +# account: +# workflow-owner-address: "0x123..." # Optional: Owner wallet/MSIG address (used for --unsigned transactions) +# rpcs: +# - chain-name: ethereum-mainnet # Required: Chain RPC endpoints +# url: "https://mainnet.infura.io/v3/KEY" + +# ========================================================================== +staging-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + +# ========================================================================== +production-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + - chain-name: ethereum-mainnet + url: https://mainnet.infura.io/v3/ diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/secrets.yaml b/building-blocks/indexer-block-trigger/block-trigger-ts/secrets.yaml new file mode 100644 index 00000000..63307f2f --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/secrets.yaml @@ -0,0 +1,3 @@ +secretsNames: + SECRET_ADDRESS: + - SECRET_ADDRESS_ALL diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/README.md b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/README.md new file mode 100644 index 00000000..f9957c0c --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/README.md @@ -0,0 +1,50 @@ +# CRE Indexer Block Trigger Workflow (TypeScript) + +This workflow processes new blocks and transactions using block-triggered webhooks (e.g., Alchemy Notify) and matches against watched addresses. It demonstrates the **block trigger pattern** in TypeScript. + +## Features +- Uses HTTP trigger from CRE TypeScript SDK +- Matches transactions to watched addresses from config +- Returns formatted JSON summary of block and matched transactions + +## Setup and Prerequisites +1. Install CRE CLI +2. Login: `cre login` +3. Install Bun (or Node.js) +4. Run `bun install` in the workflow directory + +## Running the Workflow +```bash +cd building-blocks/indexer-block-trigger/block-trigger-ts/workflow +bun install +cre workflow simulate workflow --non-interactive --trigger-index 0 --http-payload test-block.json --target staging-settings +``` + +## Example Output +```json +{ + "blockNumber": 12345678, + "blockHash": "0xabc...", + "timestamp": 1700000000, + "totalLogs": 42, + "uniqueTransactions": 10, + "matchedTransactions": 2, + "transactions": [ + { + "hash": "0xdef...", + "from": "0x...", + "to": "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "value": "1000000000000000000" + } + ] +} +``` + +## Example Use Cases +- Monitoring high-value addresses +- Contract interaction tracking +- Block-level analytics + +## Reference Documentation +- [CRE Documentation](https://docs.chain.link/cre) +- [Alchemy Webhooks](https://www.alchemy.com/docs/reference/custom-webhook) diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.production.json b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.production.json new file mode 100644 index 00000000..612d1319 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.production.json @@ -0,0 +1,6 @@ +{ + "watchedAddresses": [ + "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "0x6edce65403992e310a62460808c4b910d972f10f" + ] +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.staging.json b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.staging.json new file mode 100644 index 00000000..612d1319 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.staging.json @@ -0,0 +1,6 @@ +{ + "watchedAddresses": [ + "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "0x6edce65403992e310a62460808c4b910d972f10f" + ] +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/main.ts b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/main.ts new file mode 100644 index 00000000..b289006d --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/main.ts @@ -0,0 +1,103 @@ +import { cre, Runner, type Runtime, type HTTPPayload, decodeJson } from "@chainlink/cre-sdk"; +import type { Config, AlchemyWebhookPayload, Transaction, TransactionStore } from "./types/types"; + +function createTransactionStore(addresses: string[]): TransactionStore { + return { + watchedAddresses: new Set(addresses.map(addr => addr.toLowerCase())), + transactions: new Map() + }; +} + +function isWatchedAddress(store: TransactionStore, address: string | null): boolean { + if (!address) return false; + return store.watchedAddresses.has(address.toLowerCase()); +} + +function saveTransaction(store: TransactionStore, tx: Transaction): void { + store.transactions.set(tx.hash, tx); +} + +const onHttpTrigger = (runtime: Runtime, payload: HTTPPayload): string => { + const blockData = decodeJson(payload.input) as AlchemyWebhookPayload; + + if (!blockData.event?.data?.block) { + runtime.log("Invalid webhook payload: missing block data"); + throw new Error("Error: Invalid webhook payload structure"); + } + + const block = blockData.event.data.block; + runtime.log(`Processing block ${block.number} | hash=${block.hash}`); + + const store = createTransactionStore(runtime.config.watchedAddresses); + + const processedHashes = new Set(); + const matchedTransactions: Transaction[] = []; + + for (const log of block.logs) { + const tx = log.transaction; + + if (processedHashes.has(tx.hash)) { + continue; + } + processedHashes.add(tx.hash); + + const toAddress = tx.to?.address || null; + if (isWatchedAddress(store, toAddress)) { + runtime.log(`Match found! Transaction ${tx.hash} to watched address ${toAddress}`); + + const transaction: Transaction = { + hash: tx.hash, + nonce: tx.nonce, + index: tx.index, + from: tx.from.address, + to: toAddress, + value: tx.value, + gasPrice: tx.gasPrice, + gas: tx.gas, + status: tx.status, + gasUsed: tx.gasUsed, + blockNumber: block.number, + blockHash: block.hash, + timestamp: block.timestamp, + }; + + saveTransaction(store, transaction); + matchedTransactions.push(transaction); + } + } + + runtime.log( + `Block processing complete | ` + + `block=${block.number} | ` + + `totalLogs=${block.logs.length} | ` + + `uniqueTransactions=${processedHashes.size} | ` + + `matchedTransactions=${matchedTransactions.length}` + ); + + const result = { + blockNumber: block.number, + blockHash: block.hash, + timestamp: block.timestamp, + totalLogs: block.logs.length, + uniqueTransactions: processedHashes.size, + matchedTransactions: matchedTransactions.length, + transactions: matchedTransactions, + }; + + return JSON.stringify(result, null, 2); +}; + +const initWorkflow = (config: Config) => { + const http = new cre.capabilities.HTTPCapability(); + + return [ + cre.handler(http.trigger({}), onHttpTrigger), + ]; +}; + +export async function main() { + const runner = await Runner.newRunner(); + await runner.run(initWorkflow); +} + +main(); diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/package.json b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/package.json new file mode 100644 index 00000000..c39e19d2 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/package.json @@ -0,0 +1,16 @@ +{ + "name": "typescript-simple-template", + "version": "1.0.0", + "main": "dist/main.js", + "private": true, + "scripts": { + "postinstall": "bunx cre-setup" + }, + "license": "UNLICENSED", + "dependencies": { + "@chainlink/cre-sdk": "^1.0.0" + }, + "devDependencies": { + "@types/bun": "1.2.21" + } +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/test-block.json b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/test-block.json new file mode 100644 index 00000000..2ea91215 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/test-block.json @@ -0,0 +1,46 @@ +{ + "webhookId": "wh_test", + "id": "whevt_test", + "createdAt": "2025-11-25T16:07:15.266Z", + "type": "GRAPHQL", + "event": { + "data": { + "block": { + "hash": "0xf49c1958f057aae44d4636511744fd5f66e0a1572c68c3b6f89c6460b87b3d08", + "number": 9704813, + "timestamp": 1764086832, + "logs": [ + { + "data": "0x", + "topics": ["0xtest"], + "index": 0, + "account": { + "address": "0x4c4df78f0af62846259c6a5678498f6f2a9012b9" + }, + "transaction": { + "hash": "0xb3d49123ead828e1c7d4830c7c08b09e671a2ada491a57613dc95a6d9db377bc", + "nonce": 8617, + "index": 3, + "from": { + "address": "0xc5d959a56de33f79f318c806ca069652769d7e75" + }, + "to": { + "address": "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc" + }, + "value": "0x0", + "gasPrice": "0x7735940c", + "maxFeePerGas": "0x2540be400", + "maxPriorityFeePerGas": "0x77359400", + "gas": 15000000, + "status": 1, + "gasUsed": 116097, + "cumulativeGasUsed": 589096, + "effectiveGasPrice": "0x7735940c", + "createdContract": null + } + } + ] + } + } + } +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/tsconfig.json b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/tsconfig.json new file mode 100644 index 00000000..840fdc79 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "main.ts" + ] +} diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/types/types.ts b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/types/types.ts new file mode 100644 index 00000000..143f3daa --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/types/types.ts @@ -0,0 +1,71 @@ +export type Config = { + watchedAddresses: string[]; +}; + +export type AlchemyWebhookPayload = { + webhookId: string; + id: string; + createdAt: string; + type: string; + event: { + data: { + block: { + hash: string; + number: number; + timestamp: number; + logs: Array<{ + data: string; + topics: string[]; + index: number; + account: { + address: string; + }; + transaction: { + hash: string; + nonce: number; + index: number; + from: { + address: string; + }; + to: { + address: string; + } | null; + value: string; + gasPrice: string; + maxFeePerGas: string | null; + maxPriorityFeePerGas: string | null; + gas: number; + status: number; + gasUsed: number; + cumulativeGasUsed: number; + effectiveGasPrice: string; + createdContract?: { + address: string; + }; + }; + }>; + }; + }; + }; +}; + +export type Transaction = { + hash: string; + nonce: number; + index: number; + from: string; + to: string | null; + value: string; + gasPrice: string; + gas: number; + status: number; + gasUsed: number; + blockNumber: number; + blockHash: string; + timestamp: number; +}; + +export type TransactionStore = { + watchedAddresses: Set; + transactions: Map; +}; diff --git a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/workflow.yaml b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/workflow.yaml new file mode 100644 index 00000000..bf3a437f --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/workflow.yaml @@ -0,0 +1,34 @@ +# ========================================================================== +# CRE WORKFLOW SETTINGS FILE +# ========================================================================== +# Workflow-specific settings for CRE CLI targets. +# Each target defines user-workflow and workflow-artifacts groups. +# Settings here override CRE Project Settings File values. +# +# Example custom target: +# my-target: +# user-workflow: +# workflow-name: "MyExampleWorkflow" # Required: Workflow Registry name +# workflow-artifacts: +# workflow-path: "./main.ts" # Path to workflow entry point +# config-path: "./config.yaml" # Path to config file +# secrets-path: "../secrets.yaml" # Path to secrets file (project root by default) + +# ========================================================================== +staging-settings: + user-workflow: + workflow-name: "workflow-staging" + workflow-artifacts: + workflow-path: "./main.ts" + config-path: "./config/config.staging.json" + secrets-path: "" + + +# ========================================================================== +production-settings: + user-workflow: + workflow-name: "workflow-production" + workflow-artifacts: + workflow-path: "./main.ts" + config-path: "./config/config.production.json" + secrets-path: "" \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/README.md b/building-blocks/indexer-data-fetch/README.md new file mode 100644 index 00000000..3350a31c --- /dev/null +++ b/building-blocks/indexer-data-fetch/README.md @@ -0,0 +1,195 @@ +# CRE Indexer Data Feed Workflows + +Workflows for pulling data from The Graph indexer with scheduled cron triggers. These workflows demonstrate the **pull pattern** where the workflow initiates and fetches data on a schedule. + +## Directory Structure + +``` +building-blocks/indexer-fetch/ +├── README.md (this file) +├── indexer-fetch-go/ (Go-based workflow) +│ └── my-workflow/ +│ ├── workflow.go +│ ├── main.go +│ ├── config.staging.json +│ ├── config.production.json +│ └── workflow.yaml +└── indexer-fetch-ts/ (TypeScript-based workflow) + └── workflow/ + ├── main.ts + ├── config.staging.json + ├── config.production.json + ├── package.json + └── workflow.yaml +``` + +## Overview + +These workflows demonstrate how to: +- Query The Graph indexer using GraphQL +- Use cron triggers to schedule periodic data fetching +- Process and return JSON-formatted indexer data +- Implement the same functionality in both Go and TypeScript + +Both workflows query the Uniswap V4 subgraph on The Graph and fetch: +- Pool manager statistics (pool count, transaction count, total volume) +- ETH price data from bundles + +## Workflows + +### 1. indexer-fetch-go (Go Implementation) + +**Language:** Go + +**Features:** +- Uses `http.SendRequest` pattern from CRE Go SDK +- Implements `ConsensusIdenticalAggregation` for deterministic data +- Returns formatted JSON with timestamp and endpoint info + +**Running the workflow:** +```bash +cd building-blocks/indexer-fetch/indexer-fetch-go +cre workflow simulate my-workflow --target staging-settings +``` + +### 2. indexer-fetch-ts (TypeScript Implementation) + +**Language:** TypeScript + +**Features:** +- Uses `runInNodeMode` pattern from CRE TypeScript SDK +- Implements custom first-result aggregation for deterministic data +- Returns formatted JSON with timestamp and endpoint info + +**Running the workflow:** +```bash +cd building-blocks/indexer-fetch/indexer-fetch-ts +cre workflow simulate workflow --target staging-settings +``` + +## Configuration + +Both workflows use the same configuration structure in their respective `config.staging.json` files: + +```json +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/bca58895bc60dcb319e3cbdfd989b964/subgraphs/id/Gqm2b5J85n1bhCyDMpGbtbVn4935EvvdyHdHrx3dibyj", + "query": "{ poolManagers(first: 5) { id poolCount txCount totalVolumeUSD } bundles(first: 5) { id ethPriceUSD } }", + "variables": {} +} +``` + +### Configuration Options + +- **schedule**: Cron expression in 6-field format (second minute hour day month weekday) + - `"0 * * * * *"` - Every minute at second 0 + - `"*/30 * * * * *"` - Every 30 seconds + - `"0 */5 * * * *"` - Every 5 minutes at second 0 + +- **graphqlEndpoint**: The Graph API endpoint URL + - Gateway endpoint: `https://gateway.thegraph.com/api/{api-key}/subgraphs/id/{subgraph-id}` + - Studio endpoint: `https://api.studio.thegraph.com/query/{id}/{name}/version/latest` + +- **query**: GraphQL query string + - Simple queries without variables work best + - See The Graph documentation for query syntax + +- **variables**: Object with variables for the GraphQL query (optional) + +## Setup and Testing + +### Prerequisites + +**For Go workflow:** +1. Install CRE CLI +2. Login: `cre login` +3. Install Go + +**For TypeScript workflow:** +1. Install CRE CLI +2. Login: `cre login` +3. Install Bun (or Node.js) +4. Run `bun install` in the workflow directory + +### Running the Workflows + +**Go Workflow:** +```bash +cd building-blocks/indexer-fetch/indexer-fetch-go +cre workflow simulate workflow --target staging-settings +``` + +**TypeScript Workflow:** +```bash +cd building-blocks/indexer-fetch/indexer-fetch-ts +cre workflow simulate workflow --target staging-settings +``` + +### Example Output + +Both workflows return JSON output like: + +```json +{ + "timestamp": "2025-11-18T18:43:08.452Z", + "endpoint": "https://gateway.thegraph.com/api/.../subgraphs/id/...", + "data": { + "bundles": [ + { + "ethPriceUSD": "3157.000458184067393927942592490315", + "id": "1" + } + ], + "poolManagers": [ + { + "id": "0x498581ff718922c3f8e6a244956af099b2652b2b", + "poolCount": "5123368", + "totalVolumeUSD": "5611562100.854190095192400782985064", + "txCount": "480580367" + } + ] + } +} +``` + + +## Example Use Cases + +### 1. Monitoring Uniswap V4 Pools +Query pool statistics every minute: +```json +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/{key}/subgraphs/id/{id}", + "query": "{ poolManagers(first: 5) { id poolCount totalVolumeUSD } }", + "variables": {} +} +``` + +### 2. Tracking Token Prices +Monitor token prices every 30 seconds: +```json +{ + "schedule": "*/30 * * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/{key}/subgraphs/id/{id}", + "query": "{ tokens(first: 10, orderBy: volumeUSD, orderDirection: desc) { id symbol volumeUSD } }", + "variables": {} +} +``` + +### 3. DeFi Protocol Metrics +Check protocol statistics every 5 minutes: +```json +{ + "schedule": "0 */5 * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/{key}/subgraphs/id/{id}", + "query": "{ protocols(first: 1) { totalValueLockedUSD totalVolumeUSD txCount } }", + "variables": {} +} +``` + +## Reference Documentation + +- [CRE Documentation](https://docs.chain.link/cre) +- [The Graph Documentation](https://thegraph.com/docs/) \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/.gitignore b/building-blocks/indexer-data-fetch/indexer-fetch-go/.gitignore new file mode 100644 index 00000000..03bd4129 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol new file mode 100644 index 00000000..6ac21cc2 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITypeAndVersion} from "./ITypeAndVersion.sol"; + +/// @notice BalanceReader is used to read native currency balances from one or more accounts +/// using a contract method instead of an RPC "eth_getBalance" call. +contract BalanceReader is ITypeAndVersion { + string public constant override typeAndVersion = "BalanceReader 1.0.0"; + + function getNativeBalances(address[] memory addresses) public view returns (uint256[] memory) { + uint256[] memory balances = new uint256[](addresses.length); + for (uint256 i = 0; i < addresses.length; ++i) { + balances[i] = addresses[i].balance; + } + return balances; + } +} \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol new file mode 100644 index 00000000..99abb86f --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC20 { + + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); + + function transfer(address recipient, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol new file mode 100644 index 00000000..14b5c476 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITypeAndVersion} from "./ITypeAndVersion.sol"; + +/// @notice MessageEmitter is used to emit custom messages from a contract. +/// @dev Sender may only emit a message once per block timestamp. +contract MessageEmitter is ITypeAndVersion { + string public constant override typeAndVersion = "ContractEmitter 1.0.0"; + + event MessageEmitted(address indexed emitter, uint256 indexed timestamp, string message); + + mapping(bytes32 key => string message) private s_messages; + mapping(address emitter => string message) private s_lastMessage; + + function emitMessage( + string calldata message + ) public { + require(bytes(message).length > 0, "Message cannot be empty"); + bytes32 key = _hashKey(msg.sender, block.timestamp); + require(bytes(s_messages[key]).length == 0, "Message already exists for the same sender and block timestamp"); + s_messages[key] = message; + s_lastMessage[msg.sender] = message; + emit MessageEmitted(msg.sender, block.timestamp, message); + } + + function getMessage(address emitter, uint256 timestamp) public view returns (string memory) { + bytes32 key = _hashKey(emitter, timestamp); + require(bytes(s_messages[key]).length == 0, "Message does not exist for the given sender and timestamp"); + return s_messages[key]; + } + + function getLastMessage( + address emitter + ) public view returns (string memory) { + require(bytes(s_lastMessage[emitter]).length > 0, "No last message for the given sender"); + return s_lastMessage[emitter]; + } + + function _hashKey(address emitter, uint256 timestamp) internal pure returns (bytes32) { + return keccak256(abi.encode(emitter, timestamp)); + } +} \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol new file mode 100644 index 00000000..6eeffc54 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IReceiver} from "../../keystone/interfaces/IReceiver.sol"; +import {IERC165} from "@openzeppelin/contracts@5.0.2/interfaces/IERC165.sol"; + +contract ReserveManager is IReceiver { + uint256 public lastTotalMinted; + uint256 public lastTotalReserve; + uint256 private s_requestIdCounter; + + event RequestReserveUpdate(UpdateReserves u); + + struct UpdateReserves { + uint256 totalMinted; + uint256 totalReserve; + } + + function onReport(bytes calldata, bytes calldata report) external override { + UpdateReserves memory updateReservesData = abi.decode(report, (UpdateReserves)); + lastTotalMinted = updateReservesData.totalMinted; + lastTotalReserve = updateReservesData.totalReserve; + + s_requestIdCounter++; + emit RequestReserveUpdate(updateReservesData); + } + + function supportsInterface( + bytes4 interfaceId + ) public pure virtual override returns (bool) { + return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; + } +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi new file mode 100644 index 00000000..af8ee1b6 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"}],"name":"getNativeBalances","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi new file mode 100644 index 00000000..38876a99 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi new file mode 100644 index 00000000..794ff4a3 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"emitter","type":"address"},{"indexed":true,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"string","name":"message","type":"string"}],"name":"MessageEmitted","type":"event"},{"inputs":[{"internalType":"string","name":"message","type":"string"}],"name":"emitMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"}],"name":"getLastMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi new file mode 100644 index 00000000..50709a50 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi @@ -0,0 +1,90 @@ +[ + { + "type": "function", + "name": "lastTotalMinted", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lastTotalReserve", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "onReport", + "inputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "report", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "pure" + }, + { + "type": "event", + "name": "RequestReserveUpdate", + "inputs": [ + { + "name": "u", + "type": "tuple", + "indexed": false, + "internalType": "struct ReserveManager.UpdateReserves", + "components": [ + { + "name": "totalMinted", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "totalReserve", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "anonymous": false + } +] \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go new file mode 100644 index 00000000..ac130c74 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go @@ -0,0 +1,264 @@ +// Code generated — DO NOT EDIT. + +package balance_reader + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rpc" + "google.golang.org/protobuf/types/known/emptypb" + + pb2 "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/bindings" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +var ( + _ = bytes.Equal + _ = errors.New + _ = fmt.Sprintf + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType + _ = emptypb.Empty{} + _ = pb.NewBigIntFromInt + _ = pb2.AggregationType_AGGREGATION_TYPE_COMMON_PREFIX + _ = bindings.FilterOptions{} + _ = evm.FilterLogTriggerRequest{} + _ = cre.ResponseBufferTooSmall + _ = rpc.API{} + _ = json.Unmarshal + _ = reflect.Bool +) + +var BalanceReaderMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"addresses\",\"type\":\"address[]\"}],\"name\":\"getNativeBalances\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// Structs + +// Contract Method Inputs +type GetNativeBalancesInput struct { + Addresses []common.Address +} + +// Contract Method Outputs + +// Errors + +// Events +// The Topics struct should be used as a filter (for log triggers). +// Note: It is only possible to filter on indexed fields. +// Indexed (string and bytes) fields will be of type common.Hash. +// They need to he (crypto.Keccak256) hashed and passed in. +// Indexed (tuple/slice/array) fields can be passed in as is, the EncodeTopics function will handle the hashing. +// +// The Decoded struct will be the result of calling decode (Adapt) on the log trigger result. +// Indexed dynamic type fields will be of type common.Hash. + +// Main Binding Type for BalanceReader +type BalanceReader struct { + Address common.Address + Options *bindings.ContractInitOptions + ABI *abi.ABI + client *evm.Client + Codec BalanceReaderCodec +} + +type BalanceReaderCodec interface { + EncodeGetNativeBalancesMethodCall(in GetNativeBalancesInput) ([]byte, error) + DecodeGetNativeBalancesMethodOutput(data []byte) ([]*big.Int, error) + EncodeTypeAndVersionMethodCall() ([]byte, error) + DecodeTypeAndVersionMethodOutput(data []byte) (string, error) +} + +func NewBalanceReader( + client *evm.Client, + address common.Address, + options *bindings.ContractInitOptions, +) (*BalanceReader, error) { + parsed, err := abi.JSON(strings.NewReader(BalanceReaderMetaData.ABI)) + if err != nil { + return nil, err + } + codec, err := NewCodec() + if err != nil { + return nil, err + } + return &BalanceReader{ + Address: address, + Options: options, + ABI: &parsed, + client: client, + Codec: codec, + }, nil +} + +type Codec struct { + abi *abi.ABI +} + +func NewCodec() (BalanceReaderCodec, error) { + parsed, err := abi.JSON(strings.NewReader(BalanceReaderMetaData.ABI)) + if err != nil { + return nil, err + } + return &Codec{abi: &parsed}, nil +} + +func (c *Codec) EncodeGetNativeBalancesMethodCall(in GetNativeBalancesInput) ([]byte, error) { + return c.abi.Pack("getNativeBalances", in.Addresses) +} + +func (c *Codec) DecodeGetNativeBalancesMethodOutput(data []byte) ([]*big.Int, error) { + vals, err := c.abi.Methods["getNativeBalances"].Outputs.Unpack(data) + if err != nil { + return *new([]*big.Int), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new([]*big.Int), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result []*big.Int + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new([]*big.Int), fmt.Errorf("failed to unmarshal to []*big.Int: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeTypeAndVersionMethodCall() ([]byte, error) { + return c.abi.Pack("typeAndVersion") +} + +func (c *Codec) DecodeTypeAndVersionMethodOutput(data []byte) (string, error) { + vals, err := c.abi.Methods["typeAndVersion"].Outputs.Unpack(data) + if err != nil { + return *new(string), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(string), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result string + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(string), fmt.Errorf("failed to unmarshal to string: %w", err) + } + + return result, nil +} + +func (c BalanceReader) GetNativeBalances( + runtime cre.Runtime, + args GetNativeBalancesInput, + blockNumber *big.Int, +) cre.Promise[[]*big.Int] { + calldata, err := c.Codec.EncodeGetNativeBalancesMethodCall(args) + if err != nil { + return cre.PromiseFromResult[[]*big.Int](*new([]*big.Int), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) ([]*big.Int, error) { + return c.Codec.DecodeGetNativeBalancesMethodOutput(response.Data) + }) + +} + +func (c BalanceReader) TypeAndVersion( + runtime cre.Runtime, + blockNumber *big.Int, +) cre.Promise[string] { + calldata, err := c.Codec.EncodeTypeAndVersionMethodCall() + if err != nil { + return cre.PromiseFromResult[string](*new(string), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (string, error) { + return c.Codec.DecodeTypeAndVersionMethodOutput(response.Data) + }) + +} + +func (c BalanceReader) WriteReport( + runtime cre.Runtime, + report *cre.Report, + gasConfig *evm.GasConfig, +) cre.Promise[*evm.WriteReportReply] { + return c.client.WriteReport(runtime, &evm.WriteCreReportRequest{ + Receiver: c.Address.Bytes(), + Report: report, + GasConfig: gasConfig, + }) +} + +func (c *BalanceReader) UnpackError(data []byte) (any, error) { + switch common.Bytes2Hex(data[:4]) { + default: + return nil, errors.New("unknown error selector") + } +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go new file mode 100644 index 00000000..bcd0078c --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go @@ -0,0 +1,80 @@ +// Code generated — DO NOT EDIT. + +//go:build !wasip1 + +package balance_reader + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + evmmock "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock" +) + +var ( + _ = errors.New + _ = fmt.Errorf + _ = big.NewInt + _ = common.Big1 +) + +// BalanceReaderMock is a mock implementation of BalanceReader for testing. +type BalanceReaderMock struct { + GetNativeBalances func(GetNativeBalancesInput) ([]*big.Int, error) + TypeAndVersion func() (string, error) +} + +// NewBalanceReaderMock creates a new BalanceReaderMock for testing. +func NewBalanceReaderMock(address common.Address, clientMock *evmmock.ClientCapability) *BalanceReaderMock { + mock := &BalanceReaderMock{} + + codec, err := NewCodec() + if err != nil { + panic("failed to create codec for mock: " + err.Error()) + } + + abi := codec.(*Codec).abi + _ = abi + + funcMap := map[string]func([]byte) ([]byte, error){ + string(abi.Methods["getNativeBalances"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.GetNativeBalances == nil { + return nil, errors.New("getNativeBalances method not mocked") + } + inputs := abi.Methods["getNativeBalances"].Inputs + + values, err := inputs.Unpack(payload) + if err != nil { + return nil, errors.New("Failed to unpack payload") + } + if len(values) != 1 { + return nil, errors.New("expected 1 input value") + } + + args := GetNativeBalancesInput{ + Addresses: values[0].([]common.Address), + } + + result, err := mock.GetNativeBalances(args) + if err != nil { + return nil, err + } + return abi.Methods["getNativeBalances"].Outputs.Pack(result) + }, + string(abi.Methods["typeAndVersion"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.TypeAndVersion == nil { + return nil, errors.New("typeAndVersion method not mocked") + } + result, err := mock.TypeAndVersion() + if err != nil { + return nil, err + } + return abi.Methods["typeAndVersion"].Outputs.Pack(result) + }, + } + + evmmock.AddContractMock(address, clientMock, funcMap, nil) + return mock +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go new file mode 100644 index 00000000..1a57677d --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go @@ -0,0 +1,741 @@ +// Code generated — DO NOT EDIT. + +package ierc20 + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rpc" + "google.golang.org/protobuf/types/known/emptypb" + + pb2 "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/bindings" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +var ( + _ = bytes.Equal + _ = errors.New + _ = fmt.Sprintf + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType + _ = emptypb.Empty{} + _ = pb.NewBigIntFromInt + _ = pb2.AggregationType_AGGREGATION_TYPE_COMMON_PREFIX + _ = bindings.FilterOptions{} + _ = evm.FilterLogTriggerRequest{} + _ = cre.ResponseBufferTooSmall + _ = rpc.API{} + _ = json.Unmarshal + _ = reflect.Bool +) + +var IERC20MetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// Structs + +// Contract Method Inputs +type AllowanceInput struct { + Owner common.Address + Spender common.Address +} + +type ApproveInput struct { + Spender common.Address + Amount *big.Int +} + +type BalanceOfInput struct { + Account common.Address +} + +type TransferInput struct { + Recipient common.Address + Amount *big.Int +} + +type TransferFromInput struct { + Sender common.Address + Recipient common.Address + Amount *big.Int +} + +// Contract Method Outputs + +// Errors + +// Events +// The Topics struct should be used as a filter (for log triggers). +// Note: It is only possible to filter on indexed fields. +// Indexed (string and bytes) fields will be of type common.Hash. +// They need to he (crypto.Keccak256) hashed and passed in. +// Indexed (tuple/slice/array) fields can be passed in as is, the EncodeTopics function will handle the hashing. +// +// The Decoded struct will be the result of calling decode (Adapt) on the log trigger result. +// Indexed dynamic type fields will be of type common.Hash. + +type ApprovalTopics struct { + Owner common.Address + Spender common.Address +} + +type ApprovalDecoded struct { + Owner common.Address + Spender common.Address + Value *big.Int +} + +type TransferTopics struct { + From common.Address + To common.Address +} + +type TransferDecoded struct { + From common.Address + To common.Address + Value *big.Int +} + +// Main Binding Type for IERC20 +type IERC20 struct { + Address common.Address + Options *bindings.ContractInitOptions + ABI *abi.ABI + client *evm.Client + Codec IERC20Codec +} + +type IERC20Codec interface { + EncodeAllowanceMethodCall(in AllowanceInput) ([]byte, error) + DecodeAllowanceMethodOutput(data []byte) (*big.Int, error) + EncodeApproveMethodCall(in ApproveInput) ([]byte, error) + DecodeApproveMethodOutput(data []byte) (bool, error) + EncodeBalanceOfMethodCall(in BalanceOfInput) ([]byte, error) + DecodeBalanceOfMethodOutput(data []byte) (*big.Int, error) + EncodeTotalSupplyMethodCall() ([]byte, error) + DecodeTotalSupplyMethodOutput(data []byte) (*big.Int, error) + EncodeTransferMethodCall(in TransferInput) ([]byte, error) + DecodeTransferMethodOutput(data []byte) (bool, error) + EncodeTransferFromMethodCall(in TransferFromInput) ([]byte, error) + DecodeTransferFromMethodOutput(data []byte) (bool, error) + ApprovalLogHash() []byte + EncodeApprovalTopics(evt abi.Event, values []ApprovalTopics) ([]*evm.TopicValues, error) + DecodeApproval(log *evm.Log) (*ApprovalDecoded, error) + TransferLogHash() []byte + EncodeTransferTopics(evt abi.Event, values []TransferTopics) ([]*evm.TopicValues, error) + DecodeTransfer(log *evm.Log) (*TransferDecoded, error) +} + +func NewIERC20( + client *evm.Client, + address common.Address, + options *bindings.ContractInitOptions, +) (*IERC20, error) { + parsed, err := abi.JSON(strings.NewReader(IERC20MetaData.ABI)) + if err != nil { + return nil, err + } + codec, err := NewCodec() + if err != nil { + return nil, err + } + return &IERC20{ + Address: address, + Options: options, + ABI: &parsed, + client: client, + Codec: codec, + }, nil +} + +type Codec struct { + abi *abi.ABI +} + +func NewCodec() (IERC20Codec, error) { + parsed, err := abi.JSON(strings.NewReader(IERC20MetaData.ABI)) + if err != nil { + return nil, err + } + return &Codec{abi: &parsed}, nil +} + +func (c *Codec) EncodeAllowanceMethodCall(in AllowanceInput) ([]byte, error) { + return c.abi.Pack("allowance", in.Owner, in.Spender) +} + +func (c *Codec) DecodeAllowanceMethodOutput(data []byte) (*big.Int, error) { + vals, err := c.abi.Methods["allowance"].Outputs.Unpack(data) + if err != nil { + return *new(*big.Int), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(*big.Int), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result *big.Int + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(*big.Int), fmt.Errorf("failed to unmarshal to *big.Int: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeApproveMethodCall(in ApproveInput) ([]byte, error) { + return c.abi.Pack("approve", in.Spender, in.Amount) +} + +func (c *Codec) DecodeApproveMethodOutput(data []byte) (bool, error) { + vals, err := c.abi.Methods["approve"].Outputs.Unpack(data) + if err != nil { + return *new(bool), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(bool), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(bool), fmt.Errorf("failed to unmarshal to bool: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeBalanceOfMethodCall(in BalanceOfInput) ([]byte, error) { + return c.abi.Pack("balanceOf", in.Account) +} + +func (c *Codec) DecodeBalanceOfMethodOutput(data []byte) (*big.Int, error) { + vals, err := c.abi.Methods["balanceOf"].Outputs.Unpack(data) + if err != nil { + return *new(*big.Int), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(*big.Int), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result *big.Int + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(*big.Int), fmt.Errorf("failed to unmarshal to *big.Int: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeTotalSupplyMethodCall() ([]byte, error) { + return c.abi.Pack("totalSupply") +} + +func (c *Codec) DecodeTotalSupplyMethodOutput(data []byte) (*big.Int, error) { + vals, err := c.abi.Methods["totalSupply"].Outputs.Unpack(data) + if err != nil { + return *new(*big.Int), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(*big.Int), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result *big.Int + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(*big.Int), fmt.Errorf("failed to unmarshal to *big.Int: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeTransferMethodCall(in TransferInput) ([]byte, error) { + return c.abi.Pack("transfer", in.Recipient, in.Amount) +} + +func (c *Codec) DecodeTransferMethodOutput(data []byte) (bool, error) { + vals, err := c.abi.Methods["transfer"].Outputs.Unpack(data) + if err != nil { + return *new(bool), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(bool), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(bool), fmt.Errorf("failed to unmarshal to bool: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeTransferFromMethodCall(in TransferFromInput) ([]byte, error) { + return c.abi.Pack("transferFrom", in.Sender, in.Recipient, in.Amount) +} + +func (c *Codec) DecodeTransferFromMethodOutput(data []byte) (bool, error) { + vals, err := c.abi.Methods["transferFrom"].Outputs.Unpack(data) + if err != nil { + return *new(bool), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(bool), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(bool), fmt.Errorf("failed to unmarshal to bool: %w", err) + } + + return result, nil +} + +func (c *Codec) ApprovalLogHash() []byte { + return c.abi.Events["Approval"].ID.Bytes() +} + +func (c *Codec) EncodeApprovalTopics( + evt abi.Event, + values []ApprovalTopics, +) ([]*evm.TopicValues, error) { + var ownerRule []interface{} + for _, v := range values { + if reflect.ValueOf(v.Owner).IsZero() { + ownerRule = append(ownerRule, common.Hash{}) + continue + } + fieldVal, err := bindings.PrepareTopicArg(evt.Inputs[0], v.Owner) + if err != nil { + return nil, err + } + ownerRule = append(ownerRule, fieldVal) + } + var spenderRule []interface{} + for _, v := range values { + if reflect.ValueOf(v.Spender).IsZero() { + spenderRule = append(spenderRule, common.Hash{}) + continue + } + fieldVal, err := bindings.PrepareTopicArg(evt.Inputs[1], v.Spender) + if err != nil { + return nil, err + } + spenderRule = append(spenderRule, fieldVal) + } + + rawTopics, err := abi.MakeTopics( + ownerRule, + spenderRule, + ) + if err != nil { + return nil, err + } + + topics := make([]*evm.TopicValues, len(rawTopics)+1) + topics[0] = &evm.TopicValues{ + Values: [][]byte{evt.ID.Bytes()}, + } + for i, hashList := range rawTopics { + bs := make([][]byte, len(hashList)) + for j, h := range hashList { + // don't include empty bytes if hashed value is 0x0 + if reflect.ValueOf(h).IsZero() { + bs[j] = []byte{} + } else { + bs[j] = h.Bytes() + } + } + topics[i+1] = &evm.TopicValues{Values: bs} + } + return topics, nil +} + +// DecodeApproval decodes a log into a Approval struct. +func (c *Codec) DecodeApproval(log *evm.Log) (*ApprovalDecoded, error) { + event := new(ApprovalDecoded) + if err := c.abi.UnpackIntoInterface(event, "Approval", log.Data); err != nil { + return nil, err + } + var indexed abi.Arguments + for _, arg := range c.abi.Events["Approval"].Inputs { + if arg.Indexed { + if arg.Type.T == abi.TupleTy { + // abigen throws on tuple, so converting to bytes to + // receive back the common.Hash as is instead of error + arg.Type.T = abi.BytesTy + } + indexed = append(indexed, arg) + } + } + // Convert [][]byte → []common.Hash + topics := make([]common.Hash, len(log.Topics)) + for i, t := range log.Topics { + topics[i] = common.BytesToHash(t) + } + + if err := abi.ParseTopics(event, indexed, topics[1:]); err != nil { + return nil, err + } + return event, nil +} + +func (c *Codec) TransferLogHash() []byte { + return c.abi.Events["Transfer"].ID.Bytes() +} + +func (c *Codec) EncodeTransferTopics( + evt abi.Event, + values []TransferTopics, +) ([]*evm.TopicValues, error) { + var fromRule []interface{} + for _, v := range values { + if reflect.ValueOf(v.From).IsZero() { + fromRule = append(fromRule, common.Hash{}) + continue + } + fieldVal, err := bindings.PrepareTopicArg(evt.Inputs[0], v.From) + if err != nil { + return nil, err + } + fromRule = append(fromRule, fieldVal) + } + var toRule []interface{} + for _, v := range values { + if reflect.ValueOf(v.To).IsZero() { + toRule = append(toRule, common.Hash{}) + continue + } + fieldVal, err := bindings.PrepareTopicArg(evt.Inputs[1], v.To) + if err != nil { + return nil, err + } + toRule = append(toRule, fieldVal) + } + + rawTopics, err := abi.MakeTopics( + fromRule, + toRule, + ) + if err != nil { + return nil, err + } + + topics := make([]*evm.TopicValues, len(rawTopics)+1) + topics[0] = &evm.TopicValues{ + Values: [][]byte{evt.ID.Bytes()}, + } + for i, hashList := range rawTopics { + bs := make([][]byte, len(hashList)) + for j, h := range hashList { + // don't include empty bytes if hashed value is 0x0 + if reflect.ValueOf(h).IsZero() { + bs[j] = []byte{} + } else { + bs[j] = h.Bytes() + } + } + topics[i+1] = &evm.TopicValues{Values: bs} + } + return topics, nil +} + +// DecodeTransfer decodes a log into a Transfer struct. +func (c *Codec) DecodeTransfer(log *evm.Log) (*TransferDecoded, error) { + event := new(TransferDecoded) + if err := c.abi.UnpackIntoInterface(event, "Transfer", log.Data); err != nil { + return nil, err + } + var indexed abi.Arguments + for _, arg := range c.abi.Events["Transfer"].Inputs { + if arg.Indexed { + if arg.Type.T == abi.TupleTy { + // abigen throws on tuple, so converting to bytes to + // receive back the common.Hash as is instead of error + arg.Type.T = abi.BytesTy + } + indexed = append(indexed, arg) + } + } + // Convert [][]byte → []common.Hash + topics := make([]common.Hash, len(log.Topics)) + for i, t := range log.Topics { + topics[i] = common.BytesToHash(t) + } + + if err := abi.ParseTopics(event, indexed, topics[1:]); err != nil { + return nil, err + } + return event, nil +} + +func (c IERC20) Allowance( + runtime cre.Runtime, + args AllowanceInput, + blockNumber *big.Int, +) cre.Promise[*big.Int] { + calldata, err := c.Codec.EncodeAllowanceMethodCall(args) + if err != nil { + return cre.PromiseFromResult[*big.Int](*new(*big.Int), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (*big.Int, error) { + return c.Codec.DecodeAllowanceMethodOutput(response.Data) + }) + +} + +func (c IERC20) BalanceOf( + runtime cre.Runtime, + args BalanceOfInput, + blockNumber *big.Int, +) cre.Promise[*big.Int] { + calldata, err := c.Codec.EncodeBalanceOfMethodCall(args) + if err != nil { + return cre.PromiseFromResult[*big.Int](*new(*big.Int), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (*big.Int, error) { + return c.Codec.DecodeBalanceOfMethodOutput(response.Data) + }) + +} + +func (c IERC20) TotalSupply( + runtime cre.Runtime, + blockNumber *big.Int, +) cre.Promise[*big.Int] { + calldata, err := c.Codec.EncodeTotalSupplyMethodCall() + if err != nil { + return cre.PromiseFromResult[*big.Int](*new(*big.Int), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (*big.Int, error) { + return c.Codec.DecodeTotalSupplyMethodOutput(response.Data) + }) + +} + +func (c IERC20) WriteReport( + runtime cre.Runtime, + report *cre.Report, + gasConfig *evm.GasConfig, +) cre.Promise[*evm.WriteReportReply] { + return c.client.WriteReport(runtime, &evm.WriteCreReportRequest{ + Receiver: c.Address.Bytes(), + Report: report, + GasConfig: gasConfig, + }) +} + +func (c *IERC20) UnpackError(data []byte) (any, error) { + switch common.Bytes2Hex(data[:4]) { + default: + return nil, errors.New("unknown error selector") + } +} + +// ApprovalTrigger wraps the raw log trigger and provides decoded ApprovalDecoded data +type ApprovalTrigger struct { + cre.Trigger[*evm.Log, *evm.Log] // Embed the raw trigger + contract *IERC20 // Keep reference for decoding +} + +// Adapt method that decodes the log into Approval data +func (t *ApprovalTrigger) Adapt(l *evm.Log) (*bindings.DecodedLog[ApprovalDecoded], error) { + // Decode the log using the contract's codec + decoded, err := t.contract.Codec.DecodeApproval(l) + if err != nil { + return nil, fmt.Errorf("failed to decode Approval log: %w", err) + } + + return &bindings.DecodedLog[ApprovalDecoded]{ + Log: l, // Original log + Data: *decoded, // Decoded data + }, nil +} + +func (c *IERC20) LogTriggerApprovalLog(chainSelector uint64, confidence evm.ConfidenceLevel, filters []ApprovalTopics) (cre.Trigger[*evm.Log, *bindings.DecodedLog[ApprovalDecoded]], error) { + event := c.ABI.Events["Approval"] + topics, err := c.Codec.EncodeApprovalTopics(event, filters) + if err != nil { + return nil, fmt.Errorf("failed to encode topics for Approval: %w", err) + } + + rawTrigger := evm.LogTrigger(chainSelector, &evm.FilterLogTriggerRequest{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: topics, + Confidence: confidence, + }) + + return &ApprovalTrigger{ + Trigger: rawTrigger, + contract: c, + }, nil +} + +func (c *IERC20) FilterLogsApproval(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { + if options == nil { + options = &bindings.FilterOptions{ + ToBlock: options.ToBlock, + } + } + return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ + FilterQuery: &evm.FilterQuery{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: []*evm.Topics{ + {Topic: [][]byte{c.Codec.ApprovalLogHash()}}, + }, + BlockHash: options.BlockHash, + FromBlock: pb.NewBigIntFromInt(options.FromBlock), + ToBlock: pb.NewBigIntFromInt(options.ToBlock), + }, + }) +} + +// TransferTrigger wraps the raw log trigger and provides decoded TransferDecoded data +type TransferTrigger struct { + cre.Trigger[*evm.Log, *evm.Log] // Embed the raw trigger + contract *IERC20 // Keep reference for decoding +} + +// Adapt method that decodes the log into Transfer data +func (t *TransferTrigger) Adapt(l *evm.Log) (*bindings.DecodedLog[TransferDecoded], error) { + // Decode the log using the contract's codec + decoded, err := t.contract.Codec.DecodeTransfer(l) + if err != nil { + return nil, fmt.Errorf("failed to decode Transfer log: %w", err) + } + + return &bindings.DecodedLog[TransferDecoded]{ + Log: l, // Original log + Data: *decoded, // Decoded data + }, nil +} + +func (c *IERC20) LogTriggerTransferLog(chainSelector uint64, confidence evm.ConfidenceLevel, filters []TransferTopics) (cre.Trigger[*evm.Log, *bindings.DecodedLog[TransferDecoded]], error) { + event := c.ABI.Events["Transfer"] + topics, err := c.Codec.EncodeTransferTopics(event, filters) + if err != nil { + return nil, fmt.Errorf("failed to encode topics for Transfer: %w", err) + } + + rawTrigger := evm.LogTrigger(chainSelector, &evm.FilterLogTriggerRequest{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: topics, + Confidence: confidence, + }) + + return &TransferTrigger{ + Trigger: rawTrigger, + contract: c, + }, nil +} + +func (c *IERC20) FilterLogsTransfer(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { + if options == nil { + options = &bindings.FilterOptions{ + ToBlock: options.ToBlock, + } + } + return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ + FilterQuery: &evm.FilterQuery{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: []*evm.Topics{ + {Topic: [][]byte{c.Codec.TransferLogHash()}}, + }, + BlockHash: options.BlockHash, + FromBlock: pb.NewBigIntFromInt(options.FromBlock), + ToBlock: pb.NewBigIntFromInt(options.ToBlock), + }, + }) +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go new file mode 100644 index 00000000..c87f5c7e --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go @@ -0,0 +1,106 @@ +// Code generated — DO NOT EDIT. + +//go:build !wasip1 + +package ierc20 + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + evmmock "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock" +) + +var ( + _ = errors.New + _ = fmt.Errorf + _ = big.NewInt + _ = common.Big1 +) + +// IERC20Mock is a mock implementation of IERC20 for testing. +type IERC20Mock struct { + Allowance func(AllowanceInput) (*big.Int, error) + BalanceOf func(BalanceOfInput) (*big.Int, error) + TotalSupply func() (*big.Int, error) +} + +// NewIERC20Mock creates a new IERC20Mock for testing. +func NewIERC20Mock(address common.Address, clientMock *evmmock.ClientCapability) *IERC20Mock { + mock := &IERC20Mock{} + + codec, err := NewCodec() + if err != nil { + panic("failed to create codec for mock: " + err.Error()) + } + + abi := codec.(*Codec).abi + _ = abi + + funcMap := map[string]func([]byte) ([]byte, error){ + string(abi.Methods["allowance"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.Allowance == nil { + return nil, errors.New("allowance method not mocked") + } + inputs := abi.Methods["allowance"].Inputs + + values, err := inputs.Unpack(payload) + if err != nil { + return nil, errors.New("Failed to unpack payload") + } + if len(values) != 2 { + return nil, errors.New("expected 2 input values") + } + + args := AllowanceInput{ + Owner: values[0].(common.Address), + Spender: values[1].(common.Address), + } + + result, err := mock.Allowance(args) + if err != nil { + return nil, err + } + return abi.Methods["allowance"].Outputs.Pack(result) + }, + string(abi.Methods["balanceOf"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.BalanceOf == nil { + return nil, errors.New("balanceOf method not mocked") + } + inputs := abi.Methods["balanceOf"].Inputs + + values, err := inputs.Unpack(payload) + if err != nil { + return nil, errors.New("Failed to unpack payload") + } + if len(values) != 1 { + return nil, errors.New("expected 1 input value") + } + + args := BalanceOfInput{ + Account: values[0].(common.Address), + } + + result, err := mock.BalanceOf(args) + if err != nil { + return nil, err + } + return abi.Methods["balanceOf"].Outputs.Pack(result) + }, + string(abi.Methods["totalSupply"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.TotalSupply == nil { + return nil, errors.New("totalSupply method not mocked") + } + result, err := mock.TotalSupply() + if err != nil { + return nil, err + } + return abi.Methods["totalSupply"].Outputs.Pack(result) + }, + } + + evmmock.AddContractMock(address, clientMock, funcMap, nil) + return mock +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go new file mode 100644 index 00000000..a4398171 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go @@ -0,0 +1,485 @@ +// Code generated — DO NOT EDIT. + +package message_emitter + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rpc" + "google.golang.org/protobuf/types/known/emptypb" + + pb2 "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/bindings" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +var ( + _ = bytes.Equal + _ = errors.New + _ = fmt.Sprintf + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType + _ = emptypb.Empty{} + _ = pb.NewBigIntFromInt + _ = pb2.AggregationType_AGGREGATION_TYPE_COMMON_PREFIX + _ = bindings.FilterOptions{} + _ = evm.FilterLogTriggerRequest{} + _ = cre.ResponseBufferTooSmall + _ = rpc.API{} + _ = json.Unmarshal + _ = reflect.Bool +) + +var MessageEmitterMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"emitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"message\",\"type\":\"string\"}],\"name\":\"MessageEmitted\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"message\",\"type\":\"string\"}],\"name\":\"emitMessage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"emitter\",\"type\":\"address\"}],\"name\":\"getLastMessage\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"emitter\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"name\":\"getMessage\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// Structs + +// Contract Method Inputs +type EmitMessageInput struct { + Message string +} + +type GetLastMessageInput struct { + Emitter common.Address +} + +type GetMessageInput struct { + Emitter common.Address + Timestamp *big.Int +} + +// Contract Method Outputs + +// Errors + +// Events +// The Topics struct should be used as a filter (for log triggers). +// Note: It is only possible to filter on indexed fields. +// Indexed (string and bytes) fields will be of type common.Hash. +// They need to he (crypto.Keccak256) hashed and passed in. +// Indexed (tuple/slice/array) fields can be passed in as is, the EncodeTopics function will handle the hashing. +// +// The Decoded struct will be the result of calling decode (Adapt) on the log trigger result. +// Indexed dynamic type fields will be of type common.Hash. + +type MessageEmittedTopics struct { + Emitter common.Address + Timestamp *big.Int +} + +type MessageEmittedDecoded struct { + Emitter common.Address + Timestamp *big.Int + Message string +} + +// Main Binding Type for MessageEmitter +type MessageEmitter struct { + Address common.Address + Options *bindings.ContractInitOptions + ABI *abi.ABI + client *evm.Client + Codec MessageEmitterCodec +} + +type MessageEmitterCodec interface { + EncodeEmitMessageMethodCall(in EmitMessageInput) ([]byte, error) + EncodeGetLastMessageMethodCall(in GetLastMessageInput) ([]byte, error) + DecodeGetLastMessageMethodOutput(data []byte) (string, error) + EncodeGetMessageMethodCall(in GetMessageInput) ([]byte, error) + DecodeGetMessageMethodOutput(data []byte) (string, error) + EncodeTypeAndVersionMethodCall() ([]byte, error) + DecodeTypeAndVersionMethodOutput(data []byte) (string, error) + MessageEmittedLogHash() []byte + EncodeMessageEmittedTopics(evt abi.Event, values []MessageEmittedTopics) ([]*evm.TopicValues, error) + DecodeMessageEmitted(log *evm.Log) (*MessageEmittedDecoded, error) +} + +func NewMessageEmitter( + client *evm.Client, + address common.Address, + options *bindings.ContractInitOptions, +) (*MessageEmitter, error) { + parsed, err := abi.JSON(strings.NewReader(MessageEmitterMetaData.ABI)) + if err != nil { + return nil, err + } + codec, err := NewCodec() + if err != nil { + return nil, err + } + return &MessageEmitter{ + Address: address, + Options: options, + ABI: &parsed, + client: client, + Codec: codec, + }, nil +} + +type Codec struct { + abi *abi.ABI +} + +func NewCodec() (MessageEmitterCodec, error) { + parsed, err := abi.JSON(strings.NewReader(MessageEmitterMetaData.ABI)) + if err != nil { + return nil, err + } + return &Codec{abi: &parsed}, nil +} + +func (c *Codec) EncodeEmitMessageMethodCall(in EmitMessageInput) ([]byte, error) { + return c.abi.Pack("emitMessage", in.Message) +} + +func (c *Codec) EncodeGetLastMessageMethodCall(in GetLastMessageInput) ([]byte, error) { + return c.abi.Pack("getLastMessage", in.Emitter) +} + +func (c *Codec) DecodeGetLastMessageMethodOutput(data []byte) (string, error) { + vals, err := c.abi.Methods["getLastMessage"].Outputs.Unpack(data) + if err != nil { + return *new(string), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(string), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result string + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(string), fmt.Errorf("failed to unmarshal to string: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeGetMessageMethodCall(in GetMessageInput) ([]byte, error) { + return c.abi.Pack("getMessage", in.Emitter, in.Timestamp) +} + +func (c *Codec) DecodeGetMessageMethodOutput(data []byte) (string, error) { + vals, err := c.abi.Methods["getMessage"].Outputs.Unpack(data) + if err != nil { + return *new(string), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(string), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result string + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(string), fmt.Errorf("failed to unmarshal to string: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeTypeAndVersionMethodCall() ([]byte, error) { + return c.abi.Pack("typeAndVersion") +} + +func (c *Codec) DecodeTypeAndVersionMethodOutput(data []byte) (string, error) { + vals, err := c.abi.Methods["typeAndVersion"].Outputs.Unpack(data) + if err != nil { + return *new(string), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(string), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result string + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(string), fmt.Errorf("failed to unmarshal to string: %w", err) + } + + return result, nil +} + +func (c *Codec) MessageEmittedLogHash() []byte { + return c.abi.Events["MessageEmitted"].ID.Bytes() +} + +func (c *Codec) EncodeMessageEmittedTopics( + evt abi.Event, + values []MessageEmittedTopics, +) ([]*evm.TopicValues, error) { + var emitterRule []interface{} + for _, v := range values { + if reflect.ValueOf(v.Emitter).IsZero() { + emitterRule = append(emitterRule, common.Hash{}) + continue + } + fieldVal, err := bindings.PrepareTopicArg(evt.Inputs[0], v.Emitter) + if err != nil { + return nil, err + } + emitterRule = append(emitterRule, fieldVal) + } + var timestampRule []interface{} + for _, v := range values { + if reflect.ValueOf(v.Timestamp).IsZero() { + timestampRule = append(timestampRule, common.Hash{}) + continue + } + fieldVal, err := bindings.PrepareTopicArg(evt.Inputs[1], v.Timestamp) + if err != nil { + return nil, err + } + timestampRule = append(timestampRule, fieldVal) + } + + rawTopics, err := abi.MakeTopics( + emitterRule, + timestampRule, + ) + if err != nil { + return nil, err + } + + return bindings.PrepareTopics(rawTopics, evt.ID.Bytes()), nil +} + +// DecodeMessageEmitted decodes a log into a MessageEmitted struct. +func (c *Codec) DecodeMessageEmitted(log *evm.Log) (*MessageEmittedDecoded, error) { + event := new(MessageEmittedDecoded) + if err := c.abi.UnpackIntoInterface(event, "MessageEmitted", log.Data); err != nil { + return nil, err + } + var indexed abi.Arguments + for _, arg := range c.abi.Events["MessageEmitted"].Inputs { + if arg.Indexed { + if arg.Type.T == abi.TupleTy { + // abigen throws on tuple, so converting to bytes to + // receive back the common.Hash as is instead of error + arg.Type.T = abi.BytesTy + } + indexed = append(indexed, arg) + } + } + // Convert [][]byte → []common.Hash + topics := make([]common.Hash, len(log.Topics)) + for i, t := range log.Topics { + topics[i] = common.BytesToHash(t) + } + + if err := abi.ParseTopics(event, indexed, topics[1:]); err != nil { + return nil, err + } + return event, nil +} + +func (c MessageEmitter) GetLastMessage( + runtime cre.Runtime, + args GetLastMessageInput, + blockNumber *big.Int, +) cre.Promise[string] { + calldata, err := c.Codec.EncodeGetLastMessageMethodCall(args) + if err != nil { + return cre.PromiseFromResult[string](*new(string), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (string, error) { + return c.Codec.DecodeGetLastMessageMethodOutput(response.Data) + }) + +} + +func (c MessageEmitter) GetMessage( + runtime cre.Runtime, + args GetMessageInput, + blockNumber *big.Int, +) cre.Promise[string] { + calldata, err := c.Codec.EncodeGetMessageMethodCall(args) + if err != nil { + return cre.PromiseFromResult[string](*new(string), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (string, error) { + return c.Codec.DecodeGetMessageMethodOutput(response.Data) + }) + +} + +func (c MessageEmitter) TypeAndVersion( + runtime cre.Runtime, + blockNumber *big.Int, +) cre.Promise[string] { + calldata, err := c.Codec.EncodeTypeAndVersionMethodCall() + if err != nil { + return cre.PromiseFromResult[string](*new(string), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (string, error) { + return c.Codec.DecodeTypeAndVersionMethodOutput(response.Data) + }) + +} + +func (c MessageEmitter) WriteReport( + runtime cre.Runtime, + report *cre.Report, + gasConfig *evm.GasConfig, +) cre.Promise[*evm.WriteReportReply] { + return c.client.WriteReport(runtime, &evm.WriteCreReportRequest{ + Receiver: c.Address.Bytes(), + Report: report, + GasConfig: gasConfig, + }) +} + +func (c *MessageEmitter) UnpackError(data []byte) (any, error) { + switch common.Bytes2Hex(data[:4]) { + default: + return nil, errors.New("unknown error selector") + } +} + +// MessageEmittedTrigger wraps the raw log trigger and provides decoded MessageEmittedDecoded data +type MessageEmittedTrigger struct { + cre.Trigger[*evm.Log, *evm.Log] // Embed the raw trigger + contract *MessageEmitter // Keep reference for decoding +} + +// Adapt method that decodes the log into MessageEmitted data +func (t *MessageEmittedTrigger) Adapt(l *evm.Log) (*bindings.DecodedLog[MessageEmittedDecoded], error) { + // Decode the log using the contract's codec + decoded, err := t.contract.Codec.DecodeMessageEmitted(l) + if err != nil { + return nil, fmt.Errorf("failed to decode MessageEmitted log: %w", err) + } + + return &bindings.DecodedLog[MessageEmittedDecoded]{ + Log: l, // Original log + Data: *decoded, // Decoded data + }, nil +} + +func (c *MessageEmitter) LogTriggerMessageEmittedLog(chainSelector uint64, confidence evm.ConfidenceLevel, filters []MessageEmittedTopics) (cre.Trigger[*evm.Log, *bindings.DecodedLog[MessageEmittedDecoded]], error) { + event := c.ABI.Events["MessageEmitted"] + topics, err := c.Codec.EncodeMessageEmittedTopics(event, filters) + if err != nil { + return nil, fmt.Errorf("failed to encode topics for MessageEmitted: %w", err) + } + + rawTrigger := evm.LogTrigger(chainSelector, &evm.FilterLogTriggerRequest{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: topics, + Confidence: confidence, + }) + + return &MessageEmittedTrigger{ + Trigger: rawTrigger, + contract: c, + }, nil +} + +func (c *MessageEmitter) FilterLogsMessageEmitted(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { + if options == nil { + options = &bindings.FilterOptions{ + ToBlock: options.ToBlock, + } + } + return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ + FilterQuery: &evm.FilterQuery{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: []*evm.Topics{ + {Topic: [][]byte{c.Codec.MessageEmittedLogHash()}}, + }, + BlockHash: options.BlockHash, + FromBlock: pb.NewBigIntFromInt(options.FromBlock), + ToBlock: pb.NewBigIntFromInt(options.ToBlock), + }, + }) +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go new file mode 100644 index 00000000..3e504292 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go @@ -0,0 +1,106 @@ +// Code generated — DO NOT EDIT. + +//go:build !wasip1 + +package message_emitter + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + evmmock "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock" +) + +var ( + _ = errors.New + _ = fmt.Errorf + _ = big.NewInt + _ = common.Big1 +) + +// MessageEmitterMock is a mock implementation of MessageEmitter for testing. +type MessageEmitterMock struct { + GetLastMessage func(GetLastMessageInput) (string, error) + GetMessage func(GetMessageInput) (string, error) + TypeAndVersion func() (string, error) +} + +// NewMessageEmitterMock creates a new MessageEmitterMock for testing. +func NewMessageEmitterMock(address common.Address, clientMock *evmmock.ClientCapability) *MessageEmitterMock { + mock := &MessageEmitterMock{} + + codec, err := NewCodec() + if err != nil { + panic("failed to create codec for mock: " + err.Error()) + } + + abi := codec.(*Codec).abi + _ = abi + + funcMap := map[string]func([]byte) ([]byte, error){ + string(abi.Methods["getLastMessage"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.GetLastMessage == nil { + return nil, errors.New("getLastMessage method not mocked") + } + inputs := abi.Methods["getLastMessage"].Inputs + + values, err := inputs.Unpack(payload) + if err != nil { + return nil, errors.New("Failed to unpack payload") + } + if len(values) != 1 { + return nil, errors.New("expected 1 input value") + } + + args := GetLastMessageInput{ + Emitter: values[0].(common.Address), + } + + result, err := mock.GetLastMessage(args) + if err != nil { + return nil, err + } + return abi.Methods["getLastMessage"].Outputs.Pack(result) + }, + string(abi.Methods["getMessage"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.GetMessage == nil { + return nil, errors.New("getMessage method not mocked") + } + inputs := abi.Methods["getMessage"].Inputs + + values, err := inputs.Unpack(payload) + if err != nil { + return nil, errors.New("Failed to unpack payload") + } + if len(values) != 2 { + return nil, errors.New("expected 2 input values") + } + + args := GetMessageInput{ + Emitter: values[0].(common.Address), + Timestamp: values[1].(*big.Int), + } + + result, err := mock.GetMessage(args) + if err != nil { + return nil, err + } + return abi.Methods["getMessage"].Outputs.Pack(result) + }, + string(abi.Methods["typeAndVersion"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.TypeAndVersion == nil { + return nil, errors.New("typeAndVersion method not mocked") + } + result, err := mock.TypeAndVersion() + if err != nil { + return nil, err + } + return abi.Methods["typeAndVersion"].Outputs.Pack(result) + }, + } + + evmmock.AddContractMock(address, clientMock, funcMap, nil) + return mock +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go new file mode 100644 index 00000000..89a5b9ab --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go @@ -0,0 +1,475 @@ +// Code generated — DO NOT EDIT. + +package reserve_manager + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rpc" + "google.golang.org/protobuf/types/known/emptypb" + + pb2 "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/bindings" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +var ( + _ = bytes.Equal + _ = errors.New + _ = fmt.Sprintf + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType + _ = emptypb.Empty{} + _ = pb.NewBigIntFromInt + _ = pb2.AggregationType_AGGREGATION_TYPE_COMMON_PREFIX + _ = bindings.FilterOptions{} + _ = evm.FilterLogTriggerRequest{} + _ = cre.ResponseBufferTooSmall + _ = rpc.API{} + _ = json.Unmarshal + _ = reflect.Bool +) + +var ReserveManagerMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"function\",\"name\":\"lastTotalMinted\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"lastTotalReserve\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"onReport\",\"inputs\":[{\"name\":\"\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"report\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"supportsInterface\",\"inputs\":[{\"name\":\"interfaceId\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"pure\"},{\"type\":\"event\",\"name\":\"RequestReserveUpdate\",\"inputs\":[{\"name\":\"u\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structReserveManager.UpdateReserves\",\"components\":[{\"name\":\"totalMinted\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"totalReserve\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}],\"anonymous\":false}]", +} + +// Structs +type UpdateReserves struct { + TotalMinted *big.Int + TotalReserve *big.Int +} + +// Contract Method Inputs +type OnReportInput struct { + Arg0 []byte + Report []byte +} + +type SupportsInterfaceInput struct { + InterfaceId [4]byte +} + +// Contract Method Outputs + +// Errors + +// Events +// The Topics struct should be used as a filter (for log triggers). +// Note: It is only possible to filter on indexed fields. +// Indexed (string and bytes) fields will be of type common.Hash. +// They need to he (crypto.Keccak256) hashed and passed in. +// Indexed (tuple/slice/array) fields can be passed in as is, the EncodeTopics function will handle the hashing. +// +// The Decoded struct will be the result of calling decode (Adapt) on the log trigger result. +// Indexed dynamic type fields will be of type common.Hash. + +type RequestReserveUpdateTopics struct { +} + +type RequestReserveUpdateDecoded struct { + U UpdateReserves +} + +// Main Binding Type for ReserveManager +type ReserveManager struct { + Address common.Address + Options *bindings.ContractInitOptions + ABI *abi.ABI + client *evm.Client + Codec ReserveManagerCodec +} + +type ReserveManagerCodec interface { + EncodeLastTotalMintedMethodCall() ([]byte, error) + DecodeLastTotalMintedMethodOutput(data []byte) (*big.Int, error) + EncodeLastTotalReserveMethodCall() ([]byte, error) + DecodeLastTotalReserveMethodOutput(data []byte) (*big.Int, error) + EncodeOnReportMethodCall(in OnReportInput) ([]byte, error) + EncodeSupportsInterfaceMethodCall(in SupportsInterfaceInput) ([]byte, error) + DecodeSupportsInterfaceMethodOutput(data []byte) (bool, error) + EncodeUpdateReservesStruct(in UpdateReserves) ([]byte, error) + RequestReserveUpdateLogHash() []byte + EncodeRequestReserveUpdateTopics(evt abi.Event, values []RequestReserveUpdateTopics) ([]*evm.TopicValues, error) + DecodeRequestReserveUpdate(log *evm.Log) (*RequestReserveUpdateDecoded, error) +} + +func NewReserveManager( + client *evm.Client, + address common.Address, + options *bindings.ContractInitOptions, +) (*ReserveManager, error) { + parsed, err := abi.JSON(strings.NewReader(ReserveManagerMetaData.ABI)) + if err != nil { + return nil, err + } + codec, err := NewCodec() + if err != nil { + return nil, err + } + return &ReserveManager{ + Address: address, + Options: options, + ABI: &parsed, + client: client, + Codec: codec, + }, nil +} + +type Codec struct { + abi *abi.ABI +} + +func NewCodec() (ReserveManagerCodec, error) { + parsed, err := abi.JSON(strings.NewReader(ReserveManagerMetaData.ABI)) + if err != nil { + return nil, err + } + return &Codec{abi: &parsed}, nil +} + +func (c *Codec) EncodeLastTotalMintedMethodCall() ([]byte, error) { + return c.abi.Pack("lastTotalMinted") +} + +func (c *Codec) DecodeLastTotalMintedMethodOutput(data []byte) (*big.Int, error) { + vals, err := c.abi.Methods["lastTotalMinted"].Outputs.Unpack(data) + if err != nil { + return *new(*big.Int), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(*big.Int), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result *big.Int + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(*big.Int), fmt.Errorf("failed to unmarshal to *big.Int: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeLastTotalReserveMethodCall() ([]byte, error) { + return c.abi.Pack("lastTotalReserve") +} + +func (c *Codec) DecodeLastTotalReserveMethodOutput(data []byte) (*big.Int, error) { + vals, err := c.abi.Methods["lastTotalReserve"].Outputs.Unpack(data) + if err != nil { + return *new(*big.Int), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(*big.Int), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result *big.Int + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(*big.Int), fmt.Errorf("failed to unmarshal to *big.Int: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeOnReportMethodCall(in OnReportInput) ([]byte, error) { + return c.abi.Pack("onReport", in.Arg0, in.Report) +} + +func (c *Codec) EncodeSupportsInterfaceMethodCall(in SupportsInterfaceInput) ([]byte, error) { + return c.abi.Pack("supportsInterface", in.InterfaceId) +} + +func (c *Codec) DecodeSupportsInterfaceMethodOutput(data []byte) (bool, error) { + vals, err := c.abi.Methods["supportsInterface"].Outputs.Unpack(data) + if err != nil { + return *new(bool), err + } + jsonData, err := json.Marshal(vals[0]) + if err != nil { + return *new(bool), fmt.Errorf("failed to marshal ABI result: %w", err) + } + + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return *new(bool), fmt.Errorf("failed to unmarshal to bool: %w", err) + } + + return result, nil +} + +func (c *Codec) EncodeUpdateReservesStruct(in UpdateReserves) ([]byte, error) { + tupleType, err := abi.NewType( + "tuple", "", + []abi.ArgumentMarshaling{ + {Name: "totalMinted", Type: "uint256"}, + {Name: "totalReserve", Type: "uint256"}, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to create tuple type for UpdateReserves: %w", err) + } + args := abi.Arguments{ + {Name: "updateReserves", Type: tupleType}, + } + + return args.Pack(in) +} + +func (c *Codec) RequestReserveUpdateLogHash() []byte { + return c.abi.Events["RequestReserveUpdate"].ID.Bytes() +} + +func (c *Codec) EncodeRequestReserveUpdateTopics( + evt abi.Event, + values []RequestReserveUpdateTopics, +) ([]*evm.TopicValues, error) { + + rawTopics, err := abi.MakeTopics() + if err != nil { + return nil, err + } + + topics := make([]*evm.TopicValues, len(rawTopics)+1) + topics[0] = &evm.TopicValues{ + Values: [][]byte{evt.ID.Bytes()}, + } + for i, hashList := range rawTopics { + bs := make([][]byte, len(hashList)) + for j, h := range hashList { + // don't include empty bytes if hashed value is 0x0 + if reflect.ValueOf(h).IsZero() { + bs[j] = []byte{} + } else { + bs[j] = h.Bytes() + } + } + topics[i+1] = &evm.TopicValues{Values: bs} + } + return topics, nil +} + +// DecodeRequestReserveUpdate decodes a log into a RequestReserveUpdate struct. +func (c *Codec) DecodeRequestReserveUpdate(log *evm.Log) (*RequestReserveUpdateDecoded, error) { + event := new(RequestReserveUpdateDecoded) + if err := c.abi.UnpackIntoInterface(event, "RequestReserveUpdate", log.Data); err != nil { + return nil, err + } + var indexed abi.Arguments + for _, arg := range c.abi.Events["RequestReserveUpdate"].Inputs { + if arg.Indexed { + if arg.Type.T == abi.TupleTy { + // abigen throws on tuple, so converting to bytes to + // receive back the common.Hash as is instead of error + arg.Type.T = abi.BytesTy + } + indexed = append(indexed, arg) + } + } + // Convert [][]byte → []common.Hash + topics := make([]common.Hash, len(log.Topics)) + for i, t := range log.Topics { + topics[i] = common.BytesToHash(t) + } + + if err := abi.ParseTopics(event, indexed, topics[1:]); err != nil { + return nil, err + } + return event, nil +} + +func (c ReserveManager) LastTotalMinted( + runtime cre.Runtime, + blockNumber *big.Int, +) cre.Promise[*big.Int] { + calldata, err := c.Codec.EncodeLastTotalMintedMethodCall() + if err != nil { + return cre.PromiseFromResult[*big.Int](*new(*big.Int), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (*big.Int, error) { + return c.Codec.DecodeLastTotalMintedMethodOutput(response.Data) + }) + +} + +func (c ReserveManager) LastTotalReserve( + runtime cre.Runtime, + blockNumber *big.Int, +) cre.Promise[*big.Int] { + calldata, err := c.Codec.EncodeLastTotalReserveMethodCall() + if err != nil { + return cre.PromiseFromResult[*big.Int](*new(*big.Int), err) + } + + var bn cre.Promise[*pb.BigInt] + if blockNumber == nil { + promise := c.client.HeaderByNumber(runtime, &evm.HeaderByNumberRequest{ + BlockNumber: bindings.FinalizedBlockNumber, + }) + + bn = cre.Then(promise, func(finalizedBlock *evm.HeaderByNumberReply) (*pb.BigInt, error) { + if finalizedBlock == nil || finalizedBlock.Header == nil { + return nil, errors.New("failed to get finalized block header") + } + return finalizedBlock.Header.BlockNumber, nil + }) + } else { + bn = cre.PromiseFromResult(pb.NewBigIntFromInt(blockNumber), nil) + } + + promise := cre.ThenPromise(bn, func(bn *pb.BigInt) cre.Promise[*evm.CallContractReply] { + return c.client.CallContract(runtime, &evm.CallContractRequest{ + Call: &evm.CallMsg{To: c.Address.Bytes(), Data: calldata}, + BlockNumber: bn, + }) + }) + return cre.Then(promise, func(response *evm.CallContractReply) (*big.Int, error) { + return c.Codec.DecodeLastTotalReserveMethodOutput(response.Data) + }) + +} + +func (c ReserveManager) WriteReportFromUpdateReserves( + runtime cre.Runtime, + input UpdateReserves, + gasConfig *evm.GasConfig, +) cre.Promise[*evm.WriteReportReply] { + encoded, err := c.Codec.EncodeUpdateReservesStruct(input) + if err != nil { + return cre.PromiseFromResult[*evm.WriteReportReply](nil, err) + } + promise := runtime.GenerateReport(&pb2.ReportRequest{ + EncodedPayload: encoded, + EncoderName: "evm", + SigningAlgo: "ecdsa", + HashingAlgo: "keccak256", + }) + + return cre.ThenPromise(promise, func(report *cre.Report) cre.Promise[*evm.WriteReportReply] { + return c.client.WriteReport(runtime, &evm.WriteCreReportRequest{ + Receiver: c.Address.Bytes(), + Report: report, + GasConfig: gasConfig, + }) + }) +} + +func (c ReserveManager) WriteReport( + runtime cre.Runtime, + report *cre.Report, + gasConfig *evm.GasConfig, +) cre.Promise[*evm.WriteReportReply] { + return c.client.WriteReport(runtime, &evm.WriteCreReportRequest{ + Receiver: c.Address.Bytes(), + Report: report, + GasConfig: gasConfig, + }) +} + +func (c *ReserveManager) UnpackError(data []byte) (any, error) { + switch common.Bytes2Hex(data[:4]) { + default: + return nil, errors.New("unknown error selector") + } +} + +// RequestReserveUpdateTrigger wraps the raw log trigger and provides decoded RequestReserveUpdateDecoded data +type RequestReserveUpdateTrigger struct { + cre.Trigger[*evm.Log, *evm.Log] // Embed the raw trigger + contract *ReserveManager // Keep reference for decoding +} + +// Adapt method that decodes the log into RequestReserveUpdate data +func (t *RequestReserveUpdateTrigger) Adapt(l *evm.Log) (*bindings.DecodedLog[RequestReserveUpdateDecoded], error) { + // Decode the log using the contract's codec + decoded, err := t.contract.Codec.DecodeRequestReserveUpdate(l) + if err != nil { + return nil, fmt.Errorf("failed to decode RequestReserveUpdate log: %w", err) + } + + return &bindings.DecodedLog[RequestReserveUpdateDecoded]{ + Log: l, // Original log + Data: *decoded, // Decoded data + }, nil +} + +func (c *ReserveManager) LogTriggerRequestReserveUpdateLog(chainSelector uint64, confidence evm.ConfidenceLevel, filters []RequestReserveUpdateTopics) (cre.Trigger[*evm.Log, *bindings.DecodedLog[RequestReserveUpdateDecoded]], error) { + event := c.ABI.Events["RequestReserveUpdate"] + topics, err := c.Codec.EncodeRequestReserveUpdateTopics(event, filters) + if err != nil { + return nil, fmt.Errorf("failed to encode topics for RequestReserveUpdate: %w", err) + } + + rawTrigger := evm.LogTrigger(chainSelector, &evm.FilterLogTriggerRequest{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: topics, + Confidence: confidence, + }) + + return &RequestReserveUpdateTrigger{ + Trigger: rawTrigger, + contract: c, + }, nil +} + +func (c *ReserveManager) FilterLogsRequestReserveUpdate(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { + if options == nil { + options = &bindings.FilterOptions{ + ToBlock: options.ToBlock, + } + } + return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ + FilterQuery: &evm.FilterQuery{ + Addresses: [][]byte{c.Address.Bytes()}, + Topics: []*evm.Topics{ + {Topic: [][]byte{c.Codec.RequestReserveUpdateLogHash()}}, + }, + BlockHash: options.BlockHash, + FromBlock: pb.NewBigIntFromInt(options.FromBlock), + ToBlock: pb.NewBigIntFromInt(options.ToBlock), + }, + }) +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go new file mode 100644 index 00000000..067e50a5 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go @@ -0,0 +1,66 @@ +// Code generated — DO NOT EDIT. + +//go:build !wasip1 + +package reserve_manager + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + evmmock "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock" +) + +var ( + _ = errors.New + _ = fmt.Errorf + _ = big.NewInt + _ = common.Big1 +) + +// ReserveManagerMock is a mock implementation of ReserveManager for testing. +type ReserveManagerMock struct { + LastTotalMinted func() (*big.Int, error) + LastTotalReserve func() (*big.Int, error) +} + +// NewReserveManagerMock creates a new ReserveManagerMock for testing. +func NewReserveManagerMock(address common.Address, clientMock *evmmock.ClientCapability) *ReserveManagerMock { + mock := &ReserveManagerMock{} + + codec, err := NewCodec() + if err != nil { + panic("failed to create codec for mock: " + err.Error()) + } + + abi := codec.(*Codec).abi + _ = abi + + funcMap := map[string]func([]byte) ([]byte, error){ + string(abi.Methods["lastTotalMinted"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.LastTotalMinted == nil { + return nil, errors.New("lastTotalMinted method not mocked") + } + result, err := mock.LastTotalMinted() + if err != nil { + return nil, err + } + return abi.Methods["lastTotalMinted"].Outputs.Pack(result) + }, + string(abi.Methods["lastTotalReserve"].ID[:4]): func(payload []byte) ([]byte, error) { + if mock.LastTotalReserve == nil { + return nil, errors.New("lastTotalReserve method not mocked") + } + result, err := mock.LastTotalReserve() + if err != nil { + return nil, err + } + return abi.Methods["lastTotalReserve"].Outputs.Pack(result) + }, + } + + evmmock.AddContractMock(address, clientMock, funcMap, nil) + return mock +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol new file mode 100644 index 00000000..b667084c --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol new file mode 100644 index 00000000..762eb071 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC165} from "./IERC165.sol"; + +/// @title IReceiver - receives keystone reports +/// @notice Implementations must support the IReceiver interface through ERC165. +interface IReceiver is IERC165 { + /// @notice Handles incoming keystone reports. + /// @dev If this function call reverts, it can be retried with a higher gas + /// limit. The receiver is responsible for discarding stale reports. + /// @param metadata Report's metadata. + /// @param report Workflow report. + function onReport(bytes calldata metadata, bytes calldata report) external; +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod new file mode 100644 index 00000000..4dcedaf2 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod @@ -0,0 +1,46 @@ +module indexer-workflow-ts + +go 1.24.5 + +toolchain go1.24.10 + +require ( + github.com/ethereum/go-ethereum v1.16.4 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2776a35 + github.com/smartcontractkit/cre-sdk-go v1.0.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.0.0-beta.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.0.0-beta.0 + google.golang.org/protobuf v1.36.7 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.3 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.36.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum new file mode 100644 index 00000000..11360409 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum @@ -0,0 +1,229 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU= +github.com/ethereum/c-kzg-4844/v2 v2.1.3/go.mod h1:fyNcYI/yAuLWJxf4uzVtS8VDKeoAaRM8G/+ADz/pRdA= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.4 h1:H6dU0r2p/amA7cYg6zyG9Nt2JrKKH6oX2utfcqrSpkQ= +github.com/ethereum/go-ethereum v1.16.4/go.mod h1:P7551slMFbjn2zOQaKrJShZVN/d8bGxp4/I6yZVlb5w= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2776a35 h1:hhKdzgNZT+TnohlmJODtaxlSk+jyEO79YNe8zLFtp78= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2776a35/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= +github.com/smartcontractkit/cre-sdk-go v1.0.0 h1:O52/QDmw/W8SJ7HQ9ASlVx7alSMGsewjL0Y8WZmgf5w= +github.com/smartcontractkit/cre-sdk-go v1.0.0/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0 h1:t2bzRHnqkyxvcrJKSsKPmCGLMjGO97ESgrtLCnTIEQw= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0/go.mod h1:VVJ4mvA7wOU1Ic5b/vTaBMHEUysyxd0gdPPXkAu8CmY= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.0.0-beta.0 h1:E3S3Uk4O2/cEJtgh+mDhakK3HFcDI2zeqJIsTxUWeS8= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.0.0-beta.0/go.mod h1:M83m3FsM1uqVu06OO58mKUSZJjjH8OGJsmvFpFlRDxI= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.0.0-beta.0 h1:Tui4xQVln7Qtk3CgjBRgDfihgEaAJy2t2MofghiGIDA= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.0.0-beta.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/project.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-go/project.yaml new file mode 100644 index 00000000..81012cc9 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/project.yaml @@ -0,0 +1,27 @@ +# ========================================================================== +# CRE PROJECT SETTINGS FILE +# ========================================================================== +# Project-specific settings for CRE CLI targets. +# Each target defines cre-cli, account, and rpcs groups. +# +# Example custom target: +# my-target: +# account: +# workflow-owner-address: "0x123..." # Optional: Owner wallet/MSIG address (used for --unsigned transactions) +# rpcs: +# - chain-name: ethereum-mainnet # Required: Chain RPC endpoints +# url: "https://mainnet.infura.io/v3/KEY" + +# ========================================================================== +staging-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + +# ========================================================================== +production-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + - chain-name: ethereum-mainnet + url: https://mainnet.infura.io/v3/ diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/secrets.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-go/secrets.yaml new file mode 100644 index 00000000..6468b160 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/secrets.yaml @@ -0,0 +1,3 @@ +secretsNames: + SECRET_ID: + - SECRET_VALUE diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/README.md b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/README.md new file mode 100644 index 00000000..79eea8a3 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/README.md @@ -0,0 +1,150 @@ +# Trying out the Developer PoR example + +This template provides an end-to-end Proof-of-Reserve (PoR) example (including precompiled smart contracts). It's designed to showcase key CRE capabilities and help you get started with local simulation quickly. + +Follow the steps below to run the example: + +## 1. Initialize CRE project + +Start by initializing a new CRE project. This will scaffold the necessary project structure and a template workflow. Run cre init in the directory where you'd like your CRE project to live. Note that workflow names must be exactly 10 characters long (we will relax this requirement in the future). + +Example output: +``` +Project name?: my_cre_project +✔ Development PoR Example to understand capabilities and simulate workflows +✔ Workflow name?: workflow01 +``` + +## 2. Update .env file + +You need to add a private key to the .env file. This is specifically required if you want to simulate chain writes. For that to work the key should be valid and funded. +If your workflow does not do any chain write then you can just put any dummy key as a private key. e.g. +``` +CRE_ETH_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000001 +``` + +## 3. Configure RPC endpoints + +For local simulation to interact with a chain, you must specify RPC endpoints for the chains you interact with in the `project.yaml` file. This is required for submitting transactions and reading blockchain state. + +Note: The following 7 chains are supported in local simulation (both testnet and mainnet variants): +- Ethereum (`ethereum-testnet-sepolia`, `ethereum-mainnet`) +- Base (`ethereum-testnet-sepolia-base-1`, `ethereum-mainnet-base-1`) +- Avalanche (`avalanche-testnet-fuji`, `avalanche-mainnet`) +- Polygon (`polygon-testnet-amoy`, `polygon-mainnet`) +- BNB Chain (`binance-smart-chain-testnet`, `binance-smart-chain-mainnet`) +- Arbitrum (`ethereum-testnet-sepolia-arbitrum-1`, `ethereum-mainnet-arbitrum-1`) +- Optimism (`ethereum-testnet-sepolia-optimism-1`, `ethereum-mainnet-optimism-1`) + +Add your preferred RPCs under the `rpcs` section. For chain names, refer to https://github.com/smartcontractkit/chain-selectors/blob/main/selectors.yml + +```yaml +rpcs: + - chain-name: ethereum-testnet-sepolia + url: +``` +Ensure the provided URLs point to valid RPC endpoints for the specified chains. You may use public RPC providers or set up your own node. + +## 4. Deploy contracts + +Deploy the BalanceReader, MessageEmitter, ReserveManager and SimpleERC20 contracts. You can either do this on a local chain or on a testnet using tools like cast/foundry. + +For a quick start, you can also use the pre-deployed contract addresses on Ethereum Sepolia—no action required on your part if you're just trying things out. + +For completeness, the Solidity source code for these contracts is located under projectRoot/contracts/evm/src. +- chain: `ethereum-testnet-sepolia` +- ReserveManager contract address: `0x073671aE6EAa2468c203fDE3a79dEe0836adF032` +- SimpleERC20 contract address: `0x4700A50d858Cb281847ca4Ee0938F80DEfB3F1dd` +- BalanceReader contract address: `0x4b0739c94C1389B55481cb7506c62430cA7211Cf` +- MessageEmitter contract address: `0x1d598672486ecB50685Da5497390571Ac4E93FDc` + +## 5. [Optional] Generate contract bindings + +To enable seamless interaction between the workflow and the contracts, Go bindings need to be generated from the contract ABIs. These ABIs are located in projectRoot/contracts/src/abi. Use the cre generate-bindings command to generate the bindings. + +Note: Bindings for the template is pre-generated, so you can skip this step if there is no abi/contract changes. This command must be run from the project root directory where project.yaml is located. The CLI looks for a contracts folder and a go.mod file in this directory. + +```bash +# Navigate to your project root (where project.yaml is located) +# Generate bindings for all contracts +cre generate-bindings evm + +# The bindings will be generated in contracts/evm/src/generated/ +# Each contract gets its own package subdirectory: +# - contracts/evm/src/generated/ierc20/IERC20.go +# - contracts/evm/src/generated/reserve_manager/ReserveManager.go +# - contracts/evm/src/generated/balance_reader/BalanceReader.go +# - etc. +``` + +This will create Go binding files for all the contracts (ReserveManager, SimpleERC20, BalanceReader, MessageEmitter, etc.) that can be imported and used in your workflow. + +## 6. Configure workflow + +Configure `config.json` for the workflow +- `schedule` should be set to `"0 */1 * * * *"` for every 1 minute(s) or any other cron expression you prefer, note [CRON service quotas](https://docs.chain.link/cre/service-quotas) +- `url` should be set to existing reserves HTTP endpoint API +- `tokenAddress` should be the SimpleERC20 contract address +- `reserveManagerAddress` should be the ReserveManager contract address +- `balanceReaderAddress` should be the BalanceReader contract address +- `messageEmitterAddress` should be the MessageEmitter contract address +- `chainName` should be name of selected chain (refer to https://github.com/smartcontractkit/chain-selectors/blob/main/selectors.yml) +- `gasLimit` should be the gas limit of chain write + +The config is already populated with deployed contracts in template. + +Note: Make sure your `workflow.yaml` file is pointing to the config.json, example: + +```yaml +staging-settings: + user-workflow: + workflow-name: "workflow01" + workflow-artifacts: + workflow-path: "." + config-path: "./config.json" + secrets-path: "" +``` + + +## 7. Simulate the workflow + +> **Note:** Run `go mod tidy` to update dependencies after generating bindings. +```bash +go mod tidy + +cre workflow simulate +``` + +After this you will get a set of options similar to: + +``` +🚀 Workflow simulation ready. Please select a trigger: +1. cron-trigger@1.0.0 Trigger +2. evm:ChainSelector:16015286601757825753@1.0.0 LogTrigger + +Enter your choice (1-2): +``` + +You can simulate each of the following triggers types as follows + +### 7a. Simulating Cron Trigger Workflows + +Select option 1, and the workflow should immediately execute. + +### 7b. Simulating Log Trigger Workflows + +Select option 2, and then two additional prompts will come up and you can pass in the example inputs: + +Transaction Hash: 0x9394cc015736e536da215c31e4f59486a8d85f4cfc3641e309bf00c34b2bf410 +Log Event Index: 0 + +The output will look like: +``` +🔗 EVM Trigger Configuration: +Please provide the transaction hash and event index for the EVM log event. +Enter transaction hash (0x...): 0x9394cc015736e536da215c31e4f59486a8d85f4cfc3641e309bf00c34b2bf410 +Enter event index (0-based): 0 +Fetching transaction receipt for transaction 0x9394cc015736e536da215c31e4f59486a8d85f4cfc3641e309bf00c34b2bf410... +Found log event at index 0: contract=0x1d598672486ecB50685Da5497390571Ac4E93FDc, topics=3 +Created EVM trigger log for transaction 0x9394cc015736e536da215c31e4f59486a8d85f4cfc3641e309bf00c34b2bf410, event 0 +``` diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.production.json b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.production.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.production.json @@ -0,0 +1,6 @@ +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/bca58895bc60dcb319e3cbdfd989b964/subgraphs/id/Gqm2b5J85n1bhCyDMpGbtbVn4935EvvdyHdHrx3dibyj", + "query": "{ poolManagers(first: 5) { id poolCount txCount totalVolumeUSD } bundles(first: 5) { id ethPriceUSD } }", + "variables": {} +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.staging.json b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.staging.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.staging.json @@ -0,0 +1,6 @@ +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/bca58895bc60dcb319e3cbdfd989b964/subgraphs/id/Gqm2b5J85n1bhCyDMpGbtbVn4935EvvdyHdHrx3dibyj", + "query": "{ poolManagers(first: 5) { id poolCount txCount totalVolumeUSD } bundles(first: 5) { id ethPriceUSD } }", + "variables": {} +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/main.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/main.go new file mode 100644 index 00000000..521d0223 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/main.go @@ -0,0 +1,12 @@ +//go:build wasip1 + +package main + +import ( + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" +) + +func main() { + wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow) +} \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.go new file mode 100644 index 00000000..28fb74a3 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.go @@ -0,0 +1,126 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +type Config struct { + Schedule string `json:"schedule"` + GraphqlEndpoint string `json:"graphqlEndpoint"` + Query string `json:"query"` + Variables map[string]interface{} `json:"variables,omitempty"` +} + +type GraphQLRequest struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables,omitempty"` +} + +type GraphQLResponse struct { + Data json.RawMessage `json:"data"` + Errors []interface{} `json:"errors,omitempty"` +} + +func InitWorkflow(config *Config, logger *slog.Logger, _ cre.SecretsProvider) (cre.Workflow[*Config], error) { + cronTriggerCfg := &cron.Config{ + Schedule: config.Schedule, + } + + return cre.Workflow[*Config]{ + cre.Handler( + cron.Trigger(cronTriggerCfg), + onIndexerCronTrigger, + ), + }, nil +} + +func onIndexerCronTrigger(config *Config, runtime cre.Runtime, _ *cron.Payload) (string, error) { + logger := runtime.Logger() + timestamp := time.Now().UTC().Format(time.RFC3339) + + logger.Info("Cron triggered", "timestamp", timestamp) + logger.Info("Querying The Graph indexer", "endpoint", config.GraphqlEndpoint) + + // Fetch data from The Graph using SendRequest pattern + client := &http.Client{} + logger.Info("setup client") + result, err := http.SendRequest(config, runtime, client, fetchGraphData, cre.ConsensusIdenticalAggregation[string]()).Await() + if err != nil { + logger.Error("Failed to fetch indexer data", "err", err) + return "", err + } + + logger.Info("Indexer data fetched successfully", "timestamp", timestamp) + + // Format output + output := map[string]interface{}{ + "timestamp": timestamp, + "endpoint": config.GraphqlEndpoint, + "data": json.RawMessage(result), + } + + // Return a JSON string + out, err := json.MarshalIndent(output, "", " ") + if err != nil { + return "", err + } + return string(out), nil +} + +func fetchGraphData(config *Config, logger *slog.Logger, sendRequester *http.SendRequester) (string, error) { + + // Prepare GraphQL request + gqlRequest := GraphQLRequest{ + Query: config.Query, + Variables: config.Variables, + } + + requestBody, err := json.Marshal(gqlRequest) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %w", err) + } + + // Make POST request + httpResp, err := sendRequester.SendRequest(&http.Request{ + Method: "POST", + Url: config.GraphqlEndpoint, + Headers: map[string]string{ + "Content-Type": "application/json", + "Authorization": "Bearer bca58895bc60dcb319e3cbdfd989b964", + }, + Body: requestBody, + }).Await() + + if err != nil { + return "", fmt.Errorf("HTTP request failed: %w", err) + } + + // Parse response + var gqlResponse GraphQLResponse + if err := json.Unmarshal(httpResp.Body, &gqlResponse); err != nil { + return "", fmt.Errorf("failed to decode response: %w", err) + } + + // Check for GraphQL errors + if len(gqlResponse.Errors) > 0 { + errJSON, _ := json.Marshal(gqlResponse.Errors) + logger.Error("GraphQL errors", "errors", string(errJSON)) + return "", fmt.Errorf("GraphQL query failed: %s", string(errJSON)) + } + + if gqlResponse.Data == nil { + return "", errors.New("no data returned from GraphQL query") + } + + logger.Info("Successfully fetched data from indexer") + + return string(gqlResponse.Data), nil +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.yaml new file mode 100644 index 00000000..70ac9952 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.yaml @@ -0,0 +1,34 @@ +# ========================================================================== +# CRE WORKFLOW SETTINGS FILE +# ========================================================================== +# Workflow-specific settings for CRE CLI targets. +# Each target defines user-workflow and workflow-artifacts groups. +# Settings here override CRE Project Settings File values. +# +# Example custom target: +# my-target: +# user-workflow: +# workflow-name: "MyExampleWorkflow" # Required: Workflow Registry name +# workflow-artifacts: +# workflow-path: "./main.ts" # Path to workflow entry point +# config-path: "./config.yaml" # Path to config file +# secrets-path: "../secrets.yaml" # Path to secrets file (project root by default) + +# ========================================================================== +staging-settings: + user-workflow: + workflow-name: "workflow-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config/config.staging.json" + secrets-path: "" + + +# ========================================================================== +production-settings: + user-workflow: + workflow-name: "workflow-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config/config.production.json" + secrets-path: "" \ No newline at end of file diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/.gitignore b/building-blocks/indexer-data-fetch/indexer-fetch-ts/.gitignore new file mode 100644 index 00000000..03bd4129 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/project.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-ts/project.yaml new file mode 100644 index 00000000..81012cc9 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/project.yaml @@ -0,0 +1,27 @@ +# ========================================================================== +# CRE PROJECT SETTINGS FILE +# ========================================================================== +# Project-specific settings for CRE CLI targets. +# Each target defines cre-cli, account, and rpcs groups. +# +# Example custom target: +# my-target: +# account: +# workflow-owner-address: "0x123..." # Optional: Owner wallet/MSIG address (used for --unsigned transactions) +# rpcs: +# - chain-name: ethereum-mainnet # Required: Chain RPC endpoints +# url: "https://mainnet.infura.io/v3/KEY" + +# ========================================================================== +staging-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + +# ========================================================================== +production-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + - chain-name: ethereum-mainnet + url: https://mainnet.infura.io/v3/ diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/secrets.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-ts/secrets.yaml new file mode 100644 index 00000000..63307f2f --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/secrets.yaml @@ -0,0 +1,3 @@ +secretsNames: + SECRET_ADDRESS: + - SECRET_ADDRESS_ALL diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/README.md b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/README.md new file mode 100644 index 00000000..df03f864 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/README.md @@ -0,0 +1,53 @@ +# Typescript Simple Workflow Example + +This template provides a simple Typescript workflow example. It shows how to create a simple "Hello World" workflow using Typescript. + +Steps to run the example + +## 1. Update .env file + +You need to add a private key to env file. This is specifically required if you want to simulate chain writes. For that to work the key should be valid and funded. +If your workflow does not do any chain write then you can just put any dummy key as a private key. e.g. + +``` +CRE_ETH_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000001 +``` + +Note: Make sure your `workflow.yaml` file is pointing to the config.json, example: + +```yaml +staging-settings: + user-workflow: + workflow-name: "hello-world" + workflow-artifacts: + workflow-path: "./main.ts" + config-path: "./config.json" +``` + +## 2. Install dependencies + +If `bun` is not already installed, see https://bun.com/docs/installation for installing in your environment. + +```bash +cd && bun install +``` + +Example: For a workflow directory named `hello-world` the command would be: + +```bash +cd hello-world && bun install +``` + +## 3. Simulate the workflow + +Run the command from project root directory + +```bash +cre workflow simulate --target=staging-settings +``` + +Example: For workflow named `hello-world` the command would be: + +```bash +cre workflow simulate ./hello-world --target=staging-settings +``` diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.production.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.production.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.production.json @@ -0,0 +1,6 @@ +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/bca58895bc60dcb319e3cbdfd989b964/subgraphs/id/Gqm2b5J85n1bhCyDMpGbtbVn4935EvvdyHdHrx3dibyj", + "query": "{ poolManagers(first: 5) { id poolCount txCount totalVolumeUSD } bundles(first: 5) { id ethPriceUSD } }", + "variables": {} +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.staging.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.staging.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.staging.json @@ -0,0 +1,6 @@ +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/bca58895bc60dcb319e3cbdfd989b964/subgraphs/id/Gqm2b5J85n1bhCyDMpGbtbVn4935EvvdyHdHrx3dibyj", + "query": "{ poolManagers(first: 5) { id poolCount txCount totalVolumeUSD } bundles(first: 5) { id ethPriceUSD } }", + "variables": {} +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts new file mode 100644 index 00000000..29c6a532 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts @@ -0,0 +1,108 @@ +import { + consensusIdenticalAggregation, + cre, + type HTTPSendRequester, + json, + Runner, + type Runtime, +} from '@chainlink/cre-sdk' + +type Config = { + schedule: string + graphqlEndpoint: string + query: string + variables?: Record +} + +type GraphQLRequest = { + query: string + variables?: Record +} + +type GraphQLResponse = { + data?: unknown + errors?: unknown[] +} + +const initWorkflow = (config: Config) => { + const cron = new cre.capabilities.CronCapability() + + return [cre.handler(cron.trigger({ schedule: config.schedule }), onIndexerCronTrigger)] +} + +// fetchGraphData is the function passed to the HTTP capability's sendRequest helper. +// It contains the logic for making the GraphQL request and parsing the response. +const fetchGraphData = (sendRequester: HTTPSendRequester, config: Config): string => { + // Prepare GraphQL request + const gqlRequest: GraphQLRequest = { + query: config.query, + variables: config.variables, + } + + const req = { + url: config.graphqlEndpoint, + method: 'POST' as const, + headers: { + 'Content-Type': 'application/json', + }, + body: Buffer.from(JSON.stringify(gqlRequest)).toString('base64'), + } + + // Send the request using the HTTP client + const resp = sendRequester.sendRequest(req).result() + + // Parse the GraphQL response + const gqlResponse = json(resp) as GraphQLResponse + + // Check for GraphQL errors + if (gqlResponse.errors && gqlResponse.errors.length > 0) { + throw new Error(`GraphQL query failed: ${JSON.stringify(gqlResponse.errors)}`) + } + + if (!gqlResponse.data) { + throw new Error('No data returned from GraphQL query') + } + + // Return the data as a JSON string + return JSON.stringify(gqlResponse.data) +} + +const onIndexerCronTrigger = (runtime: Runtime): string => { + const timestamp = new Date().toISOString() + + runtime.log(`Cron triggered | timestamp=${timestamp}`) + runtime.log(`Querying The Graph indexer | endpoint=${runtime.config.graphqlEndpoint}`) + + const httpClient = new cre.capabilities.HTTPClient() + + // Use sendRequest sugar to execute the offchain fetch. + // The Graph returns deterministic data across all nodes. + // We use identical aggregation since all nodes should return identical data from The Graph. + const result = httpClient + .sendRequest( + runtime, + fetchGraphData, + consensusIdenticalAggregation(), + )(runtime.config) + .result() + + runtime.log(`Indexer data fetched successfully | timestamp=${timestamp}`) + + // Format output + const output = { + timestamp, + endpoint: runtime.config.graphqlEndpoint, + data: JSON.parse(result), + } + + // Return a formatted JSON string + return JSON.stringify(output, null, 2) +} + +export async function main() { + const runner = await Runner.newRunner() + await runner.run(initWorkflow) +} + +main() + diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/package.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/package.json new file mode 100644 index 00000000..c39e19d2 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/package.json @@ -0,0 +1,16 @@ +{ + "name": "typescript-simple-template", + "version": "1.0.0", + "main": "dist/main.js", + "private": true, + "scripts": { + "postinstall": "bunx cre-setup" + }, + "license": "UNLICENSED", + "dependencies": { + "@chainlink/cre-sdk": "^1.0.0" + }, + "devDependencies": { + "@types/bun": "1.2.21" + } +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/tsconfig.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/tsconfig.json new file mode 100644 index 00000000..840fdc79 --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "main.ts" + ] +} diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/workflow.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/workflow.yaml new file mode 100644 index 00000000..bf3a437f --- /dev/null +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/workflow.yaml @@ -0,0 +1,34 @@ +# ========================================================================== +# CRE WORKFLOW SETTINGS FILE +# ========================================================================== +# Workflow-specific settings for CRE CLI targets. +# Each target defines user-workflow and workflow-artifacts groups. +# Settings here override CRE Project Settings File values. +# +# Example custom target: +# my-target: +# user-workflow: +# workflow-name: "MyExampleWorkflow" # Required: Workflow Registry name +# workflow-artifacts: +# workflow-path: "./main.ts" # Path to workflow entry point +# config-path: "./config.yaml" # Path to config file +# secrets-path: "../secrets.yaml" # Path to secrets file (project root by default) + +# ========================================================================== +staging-settings: + user-workflow: + workflow-name: "workflow-staging" + workflow-artifacts: + workflow-path: "./main.ts" + config-path: "./config/config.staging.json" + secrets-path: "" + + +# ========================================================================== +production-settings: + user-workflow: + workflow-name: "workflow-production" + workflow-artifacts: + workflow-path: "./main.ts" + config-path: "./config/config.production.json" + secrets-path: "" \ No newline at end of file diff --git a/starter-templates/custom-data-feed/cre-custom-data-feed-go/go.sum b/starter-templates/custom-data-feed/cre-custom-data-feed-go/go.sum index 3d36e09b..2a9e8112 100644 --- a/starter-templates/custom-data-feed/cre-custom-data-feed-go/go.sum +++ b/starter-templates/custom-data-feed/cre-custom-data-feed-go/go.sum @@ -177,12 +177,16 @@ github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20250918131840-564fe2776a35/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= github.com/smartcontractkit/cre-sdk-go v0.9.0 h1:MDO9HFb4tjvu4mI4gKvdO+qXP1irULxhFwlTPVBytaM= github.com/smartcontractkit/cre-sdk-go v0.9.0/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go v0.10.0/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v0.9.0 h1:0ddtacyL1aAFxIolQnbysYlJKP9FOLJc1YRFS/Z9OJA= github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v0.9.0/go.mod h1:VVJ4mvA7wOU1Ic5b/vTaBMHEUysyxd0gdPPXkAu8CmY= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v0.10.0/go.mod h1:VVJ4mvA7wOU1Ic5b/vTaBMHEUysyxd0gdPPXkAu8CmY= github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.9.0 h1:VTLdU4nZJ9L+4X0ql20rxQ06dt572A2kmGG2nVHRgiI= github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.9.0/go.mod h1:M83m3FsM1uqVu06OO58mKUSZJjjH8OGJsmvFpFlRDxI= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0/go.mod h1:M83m3FsM1uqVu06OO58mKUSZJjjH8OGJsmvFpFlRDxI= github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.9.0 h1:BWqX7Cnd6VnhHEpjfrQGEajPtAwqH4MH0D7o3iEPvvU= github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.9.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw=