From 03060e14671e247c15fccde8d779030232bb68b7 Mon Sep 17 00:00:00 2001 From: mayowa Date: Wed, 19 Nov 2025 12:42:44 +0100 Subject: [PATCH 01/12] add workflow for fetching data from indexer --- building-blocks/indexer-fetch/README.md | 220 ++++++ .../indexer-fetch/indexer-fetch-go/.gitignore | 1 + .../contracts/evm/src/BalanceReader.sol | 18 + .../contracts/evm/src/IERC20.sol | 17 + .../contracts/evm/src/MessageEmitter.sol | 43 + .../contracts/evm/src/ReserveManager.sol | 33 + .../contracts/evm/src/abi/BalanceReader.abi | 1 + .../contracts/evm/src/abi/IERC20.abi | 1 + .../contracts/evm/src/abi/MessageEmitter.abi | 1 + .../contracts/evm/src/abi/ReserveManager.abi | 90 +++ .../generated/balance_reader/BalanceReader.go | 264 +++++++ .../balance_reader/BalanceReader_mock.go | 80 ++ .../evm/src/generated/ierc20/IERC20.go | 741 ++++++++++++++++++ .../evm/src/generated/ierc20/IERC20_mock.go | 106 +++ .../message_emitter/MessageEmitter.go | 485 ++++++++++++ .../message_emitter/MessageEmitter_mock.go | 106 +++ .../reserve_manager/ReserveManager.go | 475 +++++++++++ .../reserve_manager/ReserveManager_mock.go | 66 ++ .../contracts/evm/src/keystone/IERC165.sol | 25 + .../contracts/evm/src/keystone/IReceiver.sol | 15 + .../indexer-fetch/indexer-fetch-go/go.mod | 46 ++ .../indexer-fetch/indexer-fetch-go/go.sum | 229 ++++++ .../indexer-fetch-go/project.yaml | 27 + .../indexer-fetch-go/secrets.yaml | 3 + .../indexer-fetch-go/workflow/README.md | 150 ++++ .../workflow/config/config.production.json | 6 + .../workflow/config/config.staging.json | 6 + .../indexer-fetch-go/workflow/main.go | 12 + .../indexer-fetch-go/workflow/workflow.go | 126 +++ .../indexer-fetch-go/workflow/workflow.yaml | 34 + .../workflow/workflow_test.go | 200 +++++ .../indexer-fetch/indexer-fetch-ts/.gitignore | 1 + .../indexer-fetch-ts/project.yaml | 27 + .../indexer-fetch-ts/secrets.yaml | 3 + .../indexer-fetch-ts/workflow/README.md | 53 ++ .../workflow/config/config.production.json | 6 + .../workflow/config/config.staging.json | 6 + .../indexer-fetch-ts/workflow/main.ts | 102 +++ .../indexer-fetch-ts/workflow/package.json | 16 + .../indexer-fetch-ts/workflow/tsconfig.json | 16 + .../indexer-fetch-ts/workflow/workflow.yaml | 34 + 41 files changed, 3891 insertions(+) create mode 100644 building-blocks/indexer-fetch/README.md create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/.gitignore create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/go.mod create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/go.sum create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/project.yaml create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/secrets.yaml create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/workflow/README.md create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.production.json create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.staging.json create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/workflow/main.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.yaml create mode 100644 building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow_test.go create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/.gitignore create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/project.yaml create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/secrets.yaml create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/workflow/README.md create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.production.json create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.staging.json create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/workflow/main.ts create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/workflow/package.json create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/workflow/tsconfig.json create mode 100644 building-blocks/indexer-fetch/indexer-fetch-ts/workflow/workflow.yaml diff --git a/building-blocks/indexer-fetch/README.md b/building-blocks/indexer-fetch/README.md new file mode 100644 index 00000000..a31f0ef3 --- /dev/null +++ b/building-blocks/indexer-fetch/README.md @@ -0,0 +1,220 @@ +# CRE Indexer Workflows + +Workflows for pulling data from The Graph indexer with scheduled cron triggers, created using `cre init`. + +## Directory Structure + +``` +building-blocks/cre-indexer-workflows/ +├── README.md (this file) +├── indexer-workflow-ts/ (Go-based workflow for indexer queries) +│ └── my-workflow/ +│ ├── workflow.go +│ ├── main.go +│ ├── config.staging.json +│ ├── config.production.json +│ └── workflow.yaml +└── indexer-workflow-go/ (Go hello world template) + └── my-workflow/ + └── [template files] +``` + +## Overview + +These workflows demonstrate how to: +- Query The Graph indexer using GraphQL +- Use cron triggers to schedule periodic data fetching (every minute by default) +- Process and return JSON-formatted indexer data + +## Main Workflow: indexer-workflow-ts + +**Note:** Despite the name, this uses Go (created from CRE template 1). + +### Configuration + +The workflow is configured in `my-workflow/config.staging.json`: + +```json +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", + "query": "query GetPairs($first: Int!) { pairs(first: $first, orderBy: reserveUSD, orderDirection: desc) { id token0 { id symbol name } token1 { id symbol name } reserveUSD } }", + "variables": { + "first": 3 + } +} +``` + +### Configuration Options + +- **schedule**: Cron expression in 6-field format (second minute hour day month weekday) + - `"0 * * * * *"` - Every minute at second 0 + - `"0 */5 * * * *"` - Every 5 minutes at second 0 + +- **graphqlEndpoint**: The Graph API endpoint URL + - Public endpoint: `https://api.thegraph.com/subgraphs/name/{owner}/{subgraph}` + - Studio endpoint: `https://api.studio.thegraph.com/query/{id}/{name}/version/latest` + +- **query**: GraphQL query string with optional variables + +- **variables**: Object with variables for the GraphQL query + +### Workflow Code Structure + +The workflow (`workflow.go`) includes: + +1. **Config struct**: Holds GraphQL endpoint, query, schedule, and variables +2. **InitWorkflow**: Sets up the cron trigger +3. **onIndexerCronTrigger**: Main handler that fetches data when cron fires +4. **fetchGraphData**: Makes HTTP POST request to The Graph endpoint + +### Key Implementation Details + +```go +// Uses HTTP SendRequest pattern from CRE SDK +client := &http.Client{} +result, err := http.SendRequest(config, runtime, client, fetchGraphData, nil).Await() + +// GraphQL request structure +gqlRequest := GraphQLRequest{ + Query: config.Query, + Variables: config.Variables, +} + +// Makes POST request with proper headers +httpResp, err := sendRequester.SendRequest(&http.Request{ + Method: "POST", + Url: config.GraphqlEndpoint, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + Body: requestBody, +}).Await() +``` + +## Setup and Testing + +### Prerequisites + +1. Install CRE CLI +2. Login: `cre login` +3. Go 1.23+ installed + +### Running the Workflow + +1. Navigate to the workflow directory: +```bash +cd building-blocks/indexer-workflows/indexer-workflow-ts +``` + +2. Test with simulation: +```bash +cre workflow simulate my-workflow --target staging-settings +``` + +3. When prompted, select trigger `1` (cron-trigger) + +### Expected Behavior + +**Boilerplate (PoR) workflow**: ✅ Compiles and runs successfully +**Enhanced indexer workflow**: ⚠️ Compiles successfully but encounters runtime issues with HTTP capability in simulation + +## Current Status + +### ✅ Working + +- Workflow structure and configuration +- Compilation and WASM generation +- Cron trigger setup +- GraphQL request formatting +- Error handling + +### ⚠️ Known Issues + +The enhanced indexer workflow compiles but fails at runtime with: +``` +Workflow execution failed: + error while executing at wasm backtrace +Caused by: + Exited with i32 exit status 2 +``` + +This appears to be a runtime issue with the HTTP capability in the CRE SDK during simulation. The code structure follows the correct patterns from working examples. + +## Example Use Cases + +### 1. Monitoring Uniswap Pairs +Query top liquidity pools every minute: +```json +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", + "query": "query GetPairs($first: Int!) { pairs(first: $first, orderBy: reserveUSD, orderDirection: desc) { id reserveUSD } }", + "variables": { "first": 10 } +} +``` + +### 2. Tracking Token Transfers +Monitor recent transfers every 5 minutes: +```json +{ + "schedule": "0 */5 * * * *", + "graphqlEndpoint": "https://api.thegraph.com/subgraphs/name/{owner}/{token-subgraph}", + "query": "query GetTransfers($first: Int!) { transfers(first: $first, orderBy: timestamp, orderDirection: desc) { id from to value } }", + "variables": { "first": 20 } +} +``` + +### 3. Price Feed Updates +Check prices from a DEX every minute: +```json +{ + "schedule": "0 * * * * *", + "graphqlEndpoint": "https://api.thegraph.com/subgraphs/name/{owner}/{dex-subgraph}", + "query": "query GetPrices { tokens(first: 5, orderBy: derivedETH, orderDirection: desc) { id symbol derivedETH } }", + "variables": {} +} +``` + +## Workflow Creation Process + +These workflows were created using: + +```bash +# Initialize Go workflow from template 1 (PoR example) +cre init --project-name indexer-workflow-ts -t 1 --rpc-url https://ethereum-sepolia-rpc.publicnode.com + +# Initialize Go workflow from template 2 (Hello World) +cre init --project-name indexer-workflow-go -t 2 --rpc-url https://ethereum-sepolia-rpc.publicnode.com +``` + +The first workflow was then enhanced to support The Graph indexer queries. + +## Files Modified + +From the boilerplate template, the following files were modified: + +1. **workflow.go**: Completely rewritten to query The Graph indexer +2. **config.staging.json**: Updated with Graph endpoint and query +3. **config.production.json**: Updated with Graph endpoint and query +4. **main.go**: Updated to use new Config struct + +## Reference Documentation + +- [CRE Documentation](https://docs.chain.link/cre) +- [The Graph Documentation](https://thegraph.com/docs/) +- [Cron Expression Reference](https://en.wikipedia.org/wiki/Cron) + +## Next Steps + +To resolve the runtime issues: +1. Test in actual deployment environment (not just simulation) +2. Check CRE SDK documentation for HTTP capability usage updates +3. Verify network connectivity and endpoint accessibility +4. Consider alternative HTTP request patterns supported by the SDK + +## Learn More + +For working examples: +- See `building-blocks/read-data-feeds/read-data-feeds-go` for a production-ready workflow +- Check CRE CLI help: `cre workflow simulate --help` diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/.gitignore b/building-blocks/indexer-fetch/indexer-fetch-go/.gitignore new file mode 100644 index 00000000..03bd4129 --- /dev/null +++ b/building-blocks/indexer-fetch/indexer-fetch-go/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol new file mode 100644 index 00000000..6ac21cc2 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol new file mode 100644 index 00000000..99abb86f --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol new file mode 100644 index 00000000..14b5c476 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol new file mode 100644 index 00000000..6eeffc54 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi new file mode 100644 index 00000000..af8ee1b6 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi new file mode 100644 index 00000000..38876a99 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi new file mode 100644 index 00000000..794ff4a3 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi new file mode 100644 index 00000000..50709a50 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go b/building-blocks/indexer-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-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-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go b/building-blocks/indexer-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-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-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go new file mode 100644 index 00000000..1a57677d --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go b/building-blocks/indexer-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-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-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go b/building-blocks/indexer-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-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-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go b/building-blocks/indexer-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-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-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go b/building-blocks/indexer-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-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-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go b/building-blocks/indexer-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-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-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol new file mode 100644 index 00000000..b667084c --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol b/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol new file mode 100644 index 00000000..762eb071 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/go.mod b/building-blocks/indexer-fetch/indexer-fetch-go/go.mod new file mode 100644 index 00000000..9f8e3907 --- /dev/null +++ b/building-blocks/indexer-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/shopspring/decimal v1.4.0 + 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 + github.com/stretchr/testify v1.11.1 + 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.0 // 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/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-fetch/indexer-fetch-go/go.sum b/building-blocks/indexer-fetch/indexer-fetch-go/go.sum new file mode 100644 index 00000000..4d59cc03 --- /dev/null +++ b/building-blocks/indexer-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.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/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-fetch/indexer-fetch-go/project.yaml b/building-blocks/indexer-fetch/indexer-fetch-go/project.yaml new file mode 100644 index 00000000..81012cc9 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/secrets.yaml b/building-blocks/indexer-fetch/indexer-fetch-go/secrets.yaml new file mode 100644 index 00000000..6468b160 --- /dev/null +++ b/building-blocks/indexer-fetch/indexer-fetch-go/secrets.yaml @@ -0,0 +1,3 @@ +secretsNames: + SECRET_ID: + - SECRET_VALUE diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/README.md b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/README.md new file mode 100644 index 00000000..79eea8a3 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/workflow/config/config.production.json b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.production.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/workflow/config/config.staging.json b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.staging.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/workflow/main.go b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/main.go new file mode 100644 index 00000000..521d0223 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/workflow/workflow.go b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.go new file mode 100644 index 00000000..28fb74a3 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/workflow/workflow.yaml b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.yaml new file mode 100644 index 00000000..70ac9952 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-go/workflow/workflow_test.go b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow_test.go new file mode 100644 index 00000000..61c0cd1d --- /dev/null +++ b/building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow_test.go @@ -0,0 +1,200 @@ +package main + +import ( + "context" + _ "embed" + "encoding/json" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + pb "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" + evmmock "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock" + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" + httpmock "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http/mock" + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre/testutils" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "indexer-workflow-ts/contracts/evm/src/generated/balance_reader" + "indexer-workflow-ts/contracts/evm/src/generated/ierc20" + "indexer-workflow-ts/contracts/evm/src/generated/message_emitter" +) + +var anyExecutionTime = time.Unix(1752514917, 0) + +func TestInitWorkflow(t *testing.T) { + config := makeTestConfig(t) + runtime := testutils.NewRuntime(t, testutils.Secrets{}) + + workflow, err := InitWorkflow(config, runtime.Logger(), nil) + require.NoError(t, err) + + require.Len(t, workflow, 2) // cron, log triggers + require.Equal(t, cron.Trigger(&cron.Config{}).CapabilityID(), workflow[0].CapabilityID()) +} + +func TestOnCronTrigger(t *testing.T) { + config := makeTestConfig(t) + runtime := testutils.NewRuntime(t, testutils.Secrets{ + "": {}, + }) + + // Mock HTTP client for POR data + httpMock, err := httpmock.NewClientCapability(t) + require.NoError(t, err) + httpMock.SendRequest = func(ctx context.Context, input *http.Request) (*http.Response, error) { + // Return mock POR response + porResponse := `{ + "accountName": "TrueUSD", + "totalTrust": 1000000.0, + "totalToken": 1000000.0, + "ripcord": false, + "updatedAt": "2023-01-01T00:00:00Z" + }` + return &http.Response{Body: []byte(porResponse)}, nil + } + + // Mock EVM client + chainSelector, err := config.EVMs[0].GetChainSelector() + require.NoError(t, err) + evmMock, err := evmmock.NewClientCapability(chainSelector, t) + require.NoError(t, err) + + // Set up contract mocks using generated mock contracts + evmCfg := config.EVMs[0] + + // Mock BalanceReader for fetchNativeTokenBalance + balanceReaderMock := balance_reader.NewBalanceReaderMock( + common.HexToAddress(evmCfg.BalanceReaderAddress), + evmMock, + ) + balanceReaderMock.GetNativeBalances = func(input balance_reader.GetNativeBalancesInput) ([]*big.Int, error) { + // Return mock balance for each address (same number as input addresses) + balances := make([]*big.Int, len(input.Addresses)) + for i := range input.Addresses { + balances[i] = big.NewInt(500000000000000000) // 0.5 ETH in wei + } + return balances, nil + } + + // Mock IERC20 for getTotalSupply + ierc20Mock := ierc20.NewIERC20Mock( + common.HexToAddress(evmCfg.TokenAddress), + evmMock, + ) + ierc20Mock.TotalSupply = func() (*big.Int, error) { + return big.NewInt(1000000000000000000), nil // 1 token with 18 decimals + } + + // Note: ReserveManager WriteReportFromUpdateReserves is not a read method, + // so it's handled by the EVM mock transaction system directly + evmMock.WriteReport = func(ctx context.Context, input *evm.WriteReportRequest) (*evm.WriteReportReply, error) { + return &evm.WriteReportReply{ + TxHash: common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").Bytes(), + }, nil + } + + result, err := onPORCronTrigger(config, runtime, &cron.Payload{ + ScheduledExecutionTime: timestamppb.New(anyExecutionTime), + }) + + require.NoError(t, err) + require.NotNil(t, result) + + // Check that the result contains the expected reserve value + require.Equal(t, "1000000", result) // Should match the totalToken from mock response + + // Verify expected log messages + logs := runtime.GetLogs() + assertLogContains(t, logs, `msg="fetching por"`) + assertLogContains(t, logs, `msg=ReserveInfo`) + assertLogContains(t, logs, `msg=TotalSupply`) + assertLogContains(t, logs, `msg=TotalReserveScaled`) + assertLogContains(t, logs, `msg="Native token balance"`) +} + +func TestOnLogTrigger(t *testing.T) { + config := makeTestConfig(t) + runtime := testutils.NewRuntime(t, testutils.Secrets{}) + + // Mock EVM client + chainSelector, err := config.EVMs[0].GetChainSelector() + require.NoError(t, err) + evmMock, err := evmmock.NewClientCapability(chainSelector, t) + require.NoError(t, err) + + // Mock MessageEmitter for log trigger + evmCfg := config.EVMs[0] + messageEmitterMock := message_emitter.NewMessageEmitterMock( + common.HexToAddress(evmCfg.MessageEmitterAddress), + evmMock, + ) + messageEmitterMock.GetLastMessage = func(input message_emitter.GetLastMessageInput) (string, error) { + return "Test message from contract", nil + } + + msgEmitterAbi, err := message_emitter.MessageEmitterMetaData.GetAbi() + require.NoError(t, err) + eventData, err := abi.Arguments{msgEmitterAbi.Events["MessageEmitted"].Inputs[2]}.Pack("Test message from contract") + require.NoError(t, err, "Encoding event data should not return an error") + // Create a mock log payload + mockLog := &evm.Log{ + Topics: [][]byte{ + common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234").Bytes(), // event signature + common.HexToHash("0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd").Bytes(), // emitter address (padded) + common.HexToHash("0x000000000000000000000000000000000000000000000000000000006716eb80").Bytes(), // additional topic + }, + Data: eventData, // this is not used by the test as we pass in mockLogDecoded, but encoding here for consistency + BlockNumber: pb.NewBigIntFromInt(big.NewInt(100)), + } + + mockLogDecoded := &bindings.DecodedLog[message_emitter.MessageEmittedDecoded]{ + Log: mockLog, + Data: message_emitter.MessageEmittedDecoded{ + Emitter: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), + Message: "Test message from contract", + Timestamp: big.NewInt(100), + }, + } + + result, err := onLogTrigger(config, runtime, mockLogDecoded) + require.NoError(t, err) + require.Equal(t, "Test message from contract", result) + + // Verify expected log messages + logs := runtime.GetLogs() + assertLogContains(t, logs, `msg="Message retrieved from the contract"`) + assertLogContains(t, logs, `blockNumber=100`) +} + +//go:embed config/config.production.json +var configJson []byte + +func makeTestConfig(t *testing.T) *Config { + config := &Config{} + require.NoError(t, json.Unmarshal(configJson, config)) + return config +} + +func assertLogContains(t *testing.T, logs [][]byte, substr string) { + for _, line := range logs { + if strings.Contains(string(line), substr) { + return + } + } + t.Fatalf("Expected logs to contain substring %q, but it was not found in logs:\n%s", + substr, strings.Join(func() []string { + var logStrings []string + for _, log := range logs { + logStrings = append(logStrings, string(log)) + } + return logStrings + }(), "\n")) +} diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/.gitignore b/building-blocks/indexer-fetch/indexer-fetch-ts/.gitignore new file mode 100644 index 00000000..03bd4129 --- /dev/null +++ b/building-blocks/indexer-fetch/indexer-fetch-ts/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/project.yaml b/building-blocks/indexer-fetch/indexer-fetch-ts/project.yaml new file mode 100644 index 00000000..81012cc9 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-ts/secrets.yaml b/building-blocks/indexer-fetch/indexer-fetch-ts/secrets.yaml new file mode 100644 index 00000000..63307f2f --- /dev/null +++ b/building-blocks/indexer-fetch/indexer-fetch-ts/secrets.yaml @@ -0,0 +1,3 @@ +secretsNames: + SECRET_ADDRESS: + - SECRET_ADDRESS_ALL diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/README.md b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/README.md new file mode 100644 index 00000000..df03f864 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-ts/workflow/config/config.production.json b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.production.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-ts/workflow/config/config.staging.json b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.staging.json new file mode 100644 index 00000000..a6145e52 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-ts/workflow/main.ts b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/main.ts new file mode 100644 index 00000000..31beb99b --- /dev/null +++ b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/main.ts @@ -0,0 +1,102 @@ +import { cre, Runner, type NodeRuntime, 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 runInNodeMode helper. +// It contains the logic for making the GraphQL request and parsing the response. +const fetchGraphData = (nodeRuntime: NodeRuntime): string => { + const httpClient = new cre.capabilities.HTTPClient() + + // Prepare GraphQL request + const gqlRequest: GraphQLRequest = { + query: nodeRuntime.config.query, + variables: nodeRuntime.config.variables, + } + + const requestBody = JSON.stringify(gqlRequest) + + const req = { + url: nodeRuntime.config.graphqlEndpoint, + method: "POST" as const, + headers: { + "Content-Type": "application/json", + }, + body: new TextEncoder().encode(requestBody), + } + + // Send the request using the HTTP client + const resp = httpClient.sendRequest(nodeRuntime, req).result() + + // Parse the GraphQL response + const bodyText = new TextDecoder().decode(resp.body) + const gqlResponse: GraphQLResponse = JSON.parse(bodyText) + + // Check for GraphQL errors + if (gqlResponse.errors && gqlResponse.errors.length > 0) { + nodeRuntime.log(`GraphQL errors: ${JSON.stringify(gqlResponse.errors)}`) + throw new Error(`GraphQL query failed: ${JSON.stringify(gqlResponse.errors)}`) + } + + if (!gqlResponse.data) { + throw new Error("No data returned from GraphQL query") + } + + nodeRuntime.log("Successfully fetched data from indexer") + + // 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}`) + + // Use runInNodeMode to execute the offchain fetch. + // The Graph returns deterministic data across all nodes. + // We define a simple aggregation that takes the first result since all nodes + // should return identical data from The Graph. + const firstResultAggregation = (results: string[]) => results[0] + const result = runtime.runInNodeMode(fetchGraphData, firstResultAggregation)().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-fetch/indexer-fetch-ts/workflow/package.json b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/package.json new file mode 100644 index 00000000..c39e19d2 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-ts/workflow/tsconfig.json b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/tsconfig.json new file mode 100644 index 00000000..840fdc79 --- /dev/null +++ b/building-blocks/indexer-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-fetch/indexer-fetch-ts/workflow/workflow.yaml b/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/workflow.yaml new file mode 100644 index 00000000..bf3a437f --- /dev/null +++ b/building-blocks/indexer-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 From cfb91649e652f84489d82f9e58946cf2ee8f4a57 Mon Sep 17 00:00:00 2001 From: mayowa Date: Wed, 19 Nov 2025 12:54:16 +0100 Subject: [PATCH 02/12] update readme --- building-blocks/indexer-fetch/README.md | 281 +++++++++++------- .../cre-custom-data-feed-go/go.sum | 4 + 2 files changed, 179 insertions(+), 106 deletions(-) diff --git a/building-blocks/indexer-fetch/README.md b/building-blocks/indexer-fetch/README.md index a31f0ef3..637d0fb8 100644 --- a/building-blocks/indexer-fetch/README.md +++ b/building-blocks/indexer-fetch/README.md @@ -1,47 +1,82 @@ -# CRE Indexer Workflows +# CRE Indexer Data Feed Workflows -Workflows for pulling data from The Graph indexer with scheduled cron triggers, created using `cre init`. +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/cre-indexer-workflows/ +building-blocks/indexer-fetch/ ├── README.md (this file) -├── indexer-workflow-ts/ (Go-based workflow for indexer queries) +├── indexer-fetch-go/ (Go-based workflow) │ └── my-workflow/ │ ├── workflow.go │ ├── main.go │ ├── config.staging.json │ ├── config.production.json │ └── workflow.yaml -└── indexer-workflow-go/ (Go hello world template) - └── my-workflow/ - └── [template files] +└── 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 (every minute by default) +- Use cron triggers to schedule periodic data fetching - Process and return JSON-formatted indexer data +- Implement the same functionality in both Go and TypeScript -## Main Workflow: indexer-workflow-ts +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 -**Note:** Despite the name, this uses Go (created from CRE template 1). +## Workflows -### Configuration +### 1. indexer-fetch-go (Go Implementation) -The workflow is configured in `my-workflow/config.staging.json`: +**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://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", - "query": "query GetPairs($first: Int!) { pairs(first: $first, orderBy: reserveUSD, orderDirection: desc) { id token0 { id symbol name } token1 { id symbol name } reserveUSD } }", - "variables": { - "first": 3 - } + "graphqlEndpoint": "https://gateway.thegraph.com/api/bca58895bc60dcb319e3cbdfd989b964/subgraphs/id/Gqm2b5J85n1bhCyDMpGbtbVn4935EvvdyHdHrx3dibyj", + "query": "{ poolManagers(first: 5) { id poolCount txCount totalVolumeUSD } bundles(first: 5) { id ethPriceUSD } }", + "variables": {} } ``` @@ -49,31 +84,33 @@ The workflow is configured in `my-workflow/config.staging.json`: - **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 - - Public endpoint: `https://api.thegraph.com/subgraphs/name/{owner}/{subgraph}` + - 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 with optional variables - -- **variables**: Object with variables for the GraphQL query - -### Workflow Code Structure +- **query**: GraphQL query string + - Simple queries without variables work best + - See The Graph documentation for query syntax -The workflow (`workflow.go`) includes: +- **variables**: Object with variables for the GraphQL query (optional) -1. **Config struct**: Holds GraphQL endpoint, query, schedule, and variables -2. **InitWorkflow**: Sets up the cron trigger -3. **onIndexerCronTrigger**: Main handler that fetches data when cron fires -4. **fetchGraphData**: Makes HTTP POST request to The Graph endpoint +## Key Implementation Details -### Key Implementation Details +### Go Implementation ```go -// Uses HTTP SendRequest pattern from CRE SDK +// Uses HTTP SendRequest pattern with consensus aggregation client := &http.Client{} -result, err := http.SendRequest(config, runtime, client, fetchGraphData, nil).Await() +result, err := http.SendRequest( + config, + runtime, + client, + fetchGraphData, + cre.ConsensusIdenticalAggregation[string](), +).Await() // GraphQL request structure gqlRequest := GraphQLRequest{ @@ -81,7 +118,7 @@ gqlRequest := GraphQLRequest{ Variables: config.Variables, } -// Makes POST request with proper headers +// Makes POST request httpResp, err := sendRequester.SendRequest(&http.Request{ Method: "POST", Url: config.GraphqlEndpoint, @@ -92,129 +129,161 @@ httpResp, err := sendRequester.SendRequest(&http.Request{ }).Await() ``` +### TypeScript Implementation + +```typescript +// Uses runInNodeMode pattern with custom aggregation +const firstResultAggregation = (results: string[]) => results[0] +const result = runtime.runInNodeMode( + fetchGraphData, + firstResultAggregation +)().result() + +// GraphQL request structure +const gqlRequest: GraphQLRequest = { + query: nodeRuntime.config.query, + variables: nodeRuntime.config.variables, +} + +// Makes POST request +const resp = httpClient.sendRequest(nodeRuntime, { + url: nodeRuntime.config.graphqlEndpoint, + method: "POST" as const, + headers: { + "Content-Type": "application/json", + }, + body: new TextEncoder().encode(requestBody), +}).result() +``` + ## Setup and Testing ### Prerequisites +**For Go workflow:** 1. Install CRE CLI 2. Login: `cre login` 3. Go 1.23+ installed -### Running the Workflow +**For TypeScript workflow:** +1. Install CRE CLI +2. Login: `cre login` +3. Bun installed (or Node.js) +4. Run `bun install` in the workflow directory + +### Running the Workflows -1. Navigate to the workflow directory: +**Go Workflow:** ```bash -cd building-blocks/indexer-workflows/indexer-workflow-ts +cd building-blocks/indexer-fetch/indexer-fetch-go +cre workflow simulate my-workflow --target staging-settings ``` -2. Test with simulation: +**TypeScript Workflow:** ```bash -cre workflow simulate my-workflow --target staging-settings +cd building-blocks/indexer-fetch/indexer-fetch-ts +cre workflow simulate workflow --target staging-settings ``` -3. When prompted, select trigger `1` (cron-trigger) - -### Expected Behavior +### Expected Output -**Boilerplate (PoR) workflow**: ✅ Compiles and runs successfully -**Enhanced indexer workflow**: ⚠️ Compiles successfully but encounters runtime issues with HTTP capability in simulation +Both workflows return JSON output like: -## Current Status - -### ✅ Working - -- Workflow structure and configuration -- Compilation and WASM generation -- Cron trigger setup -- GraphQL request formatting -- Error handling +```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" + } + ] + } +} +``` -### ⚠️ Known Issues +## Status -The enhanced indexer workflow compiles but fails at runtime with: -``` -Workflow execution failed: - error while executing at wasm backtrace -Caused by: - Exited with i32 exit status 2 -``` +### ✅ Both Workflows Working -This appears to be a runtime issue with the HTTP capability in the CRE SDK during simulation. The code structure follows the correct patterns from working examples. +- ✅ Workflow structure and configuration +- ✅ Compilation and WASM generation +- ✅ Cron trigger setup +- ✅ GraphQL request formatting +- ✅ HTTP requests to The Graph +- ✅ Error handling +- ✅ Successful simulation and execution -## Example Use Cases +## Example Use Cases -### 1. Monitoring Uniswap Pairs -Query top liquidity pools every minute: +### 1. Monitoring Uniswap V4 Pools +Query pool statistics every minute: ```json { "schedule": "0 * * * * *", - "graphqlEndpoint": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", - "query": "query GetPairs($first: Int!) { pairs(first: $first, orderBy: reserveUSD, orderDirection: desc) { id reserveUSD } }", - "variables": { "first": 10 } + "graphqlEndpoint": "https://gateway.thegraph.com/api/{key}/subgraphs/id/{id}", + "query": "{ poolManagers(first: 5) { id poolCount totalVolumeUSD } }", + "variables": {} } ``` -### 2. Tracking Token Transfers -Monitor recent transfers every 5 minutes: +### 2. Tracking Token Prices +Monitor token prices every 30 seconds: ```json { - "schedule": "0 */5 * * * *", - "graphqlEndpoint": "https://api.thegraph.com/subgraphs/name/{owner}/{token-subgraph}", - "query": "query GetTransfers($first: Int!) { transfers(first: $first, orderBy: timestamp, orderDirection: desc) { id from to value } }", - "variables": { "first": 20 } + "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. Price Feed Updates -Check prices from a DEX every minute: +### 3. DeFi Protocol Metrics +Check protocol statistics every 5 minutes: ```json { - "schedule": "0 * * * * *", - "graphqlEndpoint": "https://api.thegraph.com/subgraphs/name/{owner}/{dex-subgraph}", - "query": "query GetPrices { tokens(first: 5, orderBy: derivedETH, orderDirection: desc) { id symbol derivedETH } }", + "schedule": "0 */5 * * * *", + "graphqlEndpoint": "https://gateway.thegraph.com/api/{key}/subgraphs/id/{id}", + "query": "{ protocols(first: 1) { totalValueLockedUSD totalVolumeUSD txCount } }", "variables": {} } ``` -## Workflow Creation Process - -These workflows were created using: - -```bash -# Initialize Go workflow from template 1 (PoR example) -cre init --project-name indexer-workflow-ts -t 1 --rpc-url https://ethereum-sepolia-rpc.publicnode.com - -# Initialize Go workflow from template 2 (Hello World) -cre init --project-name indexer-workflow-go -t 2 --rpc-url https://ethereum-sepolia-rpc.publicnode.com -``` - -The first workflow was then enhanced to support The Graph indexer queries. - -## Files Modified - -From the boilerplate template, the following files were modified: +## Comparison: Go vs TypeScript -1. **workflow.go**: Completely rewritten to query The Graph indexer -2. **config.staging.json**: Updated with Graph endpoint and query -3. **config.production.json**: Updated with Graph endpoint and query -4. **main.go**: Updated to use new Config struct +| Feature | Go | TypeScript | +|---------|-----|------------| +| **HTTP Pattern** | `http.SendRequest` | `runInNodeMode` with `HTTPClient` | +| **Consensus** | `ConsensusIdenticalAggregation[string]()` | Custom `firstResultAggregation` | +| **Type Safety** | Compile-time with structs | Compile-time with TypeScript types | +| **Error Handling** | Go error returns | JavaScript try/catch | +| **Entry Point** | `main.go` | `main.ts` | +| **Workflow Directory** | `my-workflow/` | `workflow/` | ## Reference Documentation - [CRE Documentation](https://docs.chain.link/cre) - [The Graph Documentation](https://thegraph.com/docs/) - [Cron Expression Reference](https://en.wikipedia.org/wiki/Cron) +- [CRE TypeScript SDK](https://www.npmjs.com/package/@chainlink/cre-sdk) -## Next Steps +## Related Patterns -To resolve the runtime issues: -1. Test in actual deployment environment (not just simulation) -2. Check CRE SDK documentation for HTTP capability usage updates -3. Verify network connectivity and endpoint accessibility -4. Consider alternative HTTP request patterns supported by the SDK +This is a **pull pattern** workflow where the workflow initiates data fetching on a schedule. For the complementary **push pattern** (event-driven workflows triggered by indexer events), see the `indexer-events` building block. ## Learn More -For working examples: -- See `building-blocks/read-data-feeds/read-data-feeds-go` for a production-ready workflow +For other workflow examples: +- See `building-blocks/read-data-feeds` for reading on-chain data feeds +- See `building-blocks/kv-store` for key-value storage patterns - Check CRE CLI help: `cre workflow simulate --help` 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= From 14a17e89ae5183ac45f84b3c7465725ae8bdc371 Mon Sep 17 00:00:00 2001 From: mayowa Date: Thu, 20 Nov 2025 13:33:55 +0100 Subject: [PATCH 03/12] rename indexer folder --- building-blocks/{indexer-fetch => indexer-data-fetch}/README.md | 0 .../indexer-fetch-go/.gitignore | 0 .../indexer-fetch-go/contracts/evm/src/BalanceReader.sol | 0 .../indexer-fetch-go/contracts/evm/src/IERC20.sol | 0 .../indexer-fetch-go/contracts/evm/src/MessageEmitter.sol | 0 .../indexer-fetch-go/contracts/evm/src/ReserveManager.sol | 0 .../indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi | 0 .../indexer-fetch-go/contracts/evm/src/abi/IERC20.abi | 0 .../indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi | 0 .../indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi | 0 .../contracts/evm/src/generated/balance_reader/BalanceReader.go | 0 .../evm/src/generated/balance_reader/BalanceReader_mock.go | 0 .../indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go | 0 .../contracts/evm/src/generated/ierc20/IERC20_mock.go | 0 .../contracts/evm/src/generated/message_emitter/MessageEmitter.go | 0 .../evm/src/generated/message_emitter/MessageEmitter_mock.go | 0 .../contracts/evm/src/generated/reserve_manager/ReserveManager.go | 0 .../evm/src/generated/reserve_manager/ReserveManager_mock.go | 0 .../indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol | 0 .../indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol | 0 .../{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/go.mod | 0 .../{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/go.sum | 0 .../indexer-fetch-go/project.yaml | 0 .../indexer-fetch-go/secrets.yaml | 0 .../indexer-fetch-go/workflow/README.md | 0 .../indexer-fetch-go/workflow/config/config.production.json | 0 .../indexer-fetch-go/workflow/config/config.staging.json | 0 .../indexer-fetch-go/workflow/main.go | 0 .../indexer-fetch-go/workflow/workflow.go | 0 .../indexer-fetch-go/workflow/workflow.yaml | 0 .../indexer-fetch-go/workflow/workflow_test.go | 0 .../indexer-fetch-ts/.gitignore | 0 .../indexer-fetch-ts/project.yaml | 0 .../indexer-fetch-ts/secrets.yaml | 0 .../indexer-fetch-ts/workflow/README.md | 0 .../indexer-fetch-ts/workflow/config/config.production.json | 0 .../indexer-fetch-ts/workflow/config/config.staging.json | 0 .../indexer-fetch-ts/workflow/main.ts | 0 .../indexer-fetch-ts/workflow/package.json | 0 .../indexer-fetch-ts/workflow/tsconfig.json | 0 .../indexer-fetch-ts/workflow/workflow.yaml | 0 41 files changed, 0 insertions(+), 0 deletions(-) rename building-blocks/{indexer-fetch => indexer-data-fetch}/README.md (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/.gitignore (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/BalanceReader.sol (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/IERC20.sol (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/ReserveManager.sol (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/go.mod (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/go.sum (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/project.yaml (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/secrets.yaml (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/workflow/README.md (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/workflow/config/config.production.json (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/workflow/config/config.staging.json (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/workflow/main.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/workflow/workflow.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/workflow/workflow.yaml (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-go/workflow/workflow_test.go (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/.gitignore (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/project.yaml (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/secrets.yaml (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/workflow/README.md (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/workflow/config/config.production.json (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/workflow/config/config.staging.json (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/workflow/main.ts (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/workflow/package.json (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/workflow/tsconfig.json (100%) rename building-blocks/{indexer-fetch => indexer-data-fetch}/indexer-fetch-ts/workflow/workflow.yaml (100%) diff --git a/building-blocks/indexer-fetch/README.md b/building-blocks/indexer-data-fetch/README.md similarity index 100% rename from building-blocks/indexer-fetch/README.md rename to building-blocks/indexer-data-fetch/README.md diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/.gitignore b/building-blocks/indexer-data-fetch/indexer-fetch-go/.gitignore similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/.gitignore rename to building-blocks/indexer-data-fetch/indexer-fetch-go/.gitignore diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/BalanceReader.sol diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/IERC20.sol diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/MessageEmitter.sol diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol b/building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/ReserveManager.sol diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/BalanceReader.abi diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/IERC20.abi diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/MessageEmitter.abi diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/abi/ReserveManager.abi diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/balance_reader/BalanceReader_mock.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/ierc20/IERC20_mock.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/message_emitter/MessageEmitter_mock.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/generated/reserve_manager/ReserveManager_mock.go diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IERC165.sol diff --git a/building-blocks/indexer-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 similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol rename to building-blocks/indexer-data-fetch/indexer-fetch-go/contracts/evm/src/keystone/IReceiver.sol diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/go.mod b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/go.mod rename to building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/go.sum b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/go.sum rename to building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/project.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-go/project.yaml similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/project.yaml rename to building-blocks/indexer-data-fetch/indexer-fetch-go/project.yaml diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/secrets.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-go/secrets.yaml similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/secrets.yaml rename to building-blocks/indexer-data-fetch/indexer-fetch-go/secrets.yaml diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/README.md b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/README.md similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/workflow/README.md rename to building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/README.md diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.production.json b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.production.json similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.production.json rename to building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.production.json diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.staging.json b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.staging.json similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/workflow/config/config.staging.json rename to building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/config/config.staging.json diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/main.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/main.go similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/workflow/main.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/main.go diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.go similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.go diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.yaml similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow.yaml rename to building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow.yaml diff --git a/building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow_test.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow_test.go similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-go/workflow/workflow_test.go rename to building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow_test.go diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/.gitignore b/building-blocks/indexer-data-fetch/indexer-fetch-ts/.gitignore similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/.gitignore rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/.gitignore diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/project.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-ts/project.yaml similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/project.yaml rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/project.yaml diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/secrets.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-ts/secrets.yaml similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/secrets.yaml rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/secrets.yaml diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/README.md b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/README.md similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/workflow/README.md rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/README.md diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.production.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.production.json similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.production.json rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.production.json diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.staging.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.staging.json similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/workflow/config/config.staging.json rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/config/config.staging.json diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/main.ts b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/workflow/main.ts rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/package.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/package.json similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/workflow/package.json rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/package.json diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/tsconfig.json b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/tsconfig.json similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/workflow/tsconfig.json rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/tsconfig.json diff --git a/building-blocks/indexer-fetch/indexer-fetch-ts/workflow/workflow.yaml b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/workflow.yaml similarity index 100% rename from building-blocks/indexer-fetch/indexer-fetch-ts/workflow/workflow.yaml rename to building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/workflow.yaml From fc9aea3a7439ad2b0bfddf747c5bb69475faf638 Mon Sep 17 00:00:00 2001 From: mayowa Date: Wed, 26 Nov 2025 15:10:29 +0100 Subject: [PATCH 04/12] add block trigger to indexer workflows --- .../indexer-block-trigger/README.md | 153 +++++++++++++ .../block-trigger-go/.gitignore | 1 + .../block-trigger-go/go.mod | 19 ++ .../block-trigger-go/go.sum | 24 ++ .../block-trigger-go/project.yaml | 27 +++ .../block-trigger-go/secrets.yaml | 1 + .../block-trigger-go/workflow/README.md | 22 ++ .../workflow/config.production.json | 6 + .../workflow/config.staging.json | 6 + .../block-trigger-go/workflow/main.go | 215 ++++++++++++++++++ .../block-trigger-go/workflow/test-block.json | 46 ++++ .../block-trigger-go/workflow/workflow.yaml | 34 +++ .../block-trigger-ts/.gitignore | 1 + .../block-trigger-ts/project.yaml | 27 +++ .../block-trigger-ts/secrets.yaml | 3 + .../block-trigger-ts/workflow/README.md | 53 +++++ .../workflow/config/config.production.json | 6 + .../workflow/config/config.staging.json | 6 + .../block-trigger-ts/workflow/main.ts | 103 +++++++++ .../block-trigger-ts/workflow/package.json | 16 ++ .../block-trigger-ts/workflow/test-block.json | 46 ++++ .../block-trigger-ts/workflow/tsconfig.json | 16 ++ .../block-trigger-ts/workflow/types/types.ts | 71 ++++++ .../block-trigger-ts/workflow/workflow.yaml | 34 +++ 24 files changed, 936 insertions(+) create mode 100644 building-blocks/indexer-block-trigger/README.md create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/.gitignore create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/go.mod create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/go.sum create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/project.yaml create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/secrets.yaml create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/workflow/README.md create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.production.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/workflow/config.staging.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/workflow/main.go create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/workflow/test-block.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-go/workflow/workflow.yaml create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/.gitignore create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/project.yaml create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/secrets.yaml create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/README.md create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.production.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/config/config.staging.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/main.ts create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/package.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/test-block.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/tsconfig.json create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/types/types.ts create mode 100644 building-blocks/indexer-block-trigger/block-trigger-ts/workflow/workflow.yaml diff --git a/building-blocks/indexer-block-trigger/README.md b/building-blocks/indexer-block-trigger/README.md new file mode 100644 index 00000000..4818b9db --- /dev/null +++ b/building-blocks/indexer-block-trigger/README.md @@ -0,0 +1,153 @@ +# 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", + ... + } + ] +} +``` + +## 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..ff09cf65 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/README.md @@ -0,0 +1,22 @@ +# Blank Workflow Example + +This template provides a blank workflow example. It aims to give a starting point for writing a workflow from scratch and to get started with local simulation. + +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 +``` + +## 2. Simulate the workflow +Run the command from project root directory + +```bash +cre workflow simulate --target=staging-settings +``` + +It is recommended to look into other existing examples to see how to write a workflow. You can generate then by running the `cre init` command. 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..df03f864 --- /dev/null +++ b/building-blocks/indexer-block-trigger/block-trigger-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-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..32f816e3 --- /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"); + return "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 From 99691ddab7006e4065d4dbde81be4bb0c274bcbc Mon Sep 17 00:00:00 2001 From: mayowa Date: Wed, 26 Nov 2025 15:12:37 +0100 Subject: [PATCH 05/12] add block trigger to indexer workflows --- building-blocks/indexer-data-fetch/README.md | 104 +----------------- .../indexer-fetch-ts/workflow/main.ts | 6 +- 2 files changed, 9 insertions(+), 101 deletions(-) diff --git a/building-blocks/indexer-data-fetch/README.md b/building-blocks/indexer-data-fetch/README.md index 637d0fb8..3350a31c 100644 --- a/building-blocks/indexer-data-fetch/README.md +++ b/building-blocks/indexer-data-fetch/README.md @@ -97,65 +97,6 @@ Both workflows use the same configuration structure in their respective `config. - **variables**: Object with variables for the GraphQL query (optional) -## Key Implementation Details - -### Go Implementation - -```go -// Uses HTTP SendRequest pattern with consensus aggregation -client := &http.Client{} -result, err := http.SendRequest( - config, - runtime, - client, - fetchGraphData, - cre.ConsensusIdenticalAggregation[string](), -).Await() - -// GraphQL request structure -gqlRequest := GraphQLRequest{ - Query: config.Query, - Variables: config.Variables, -} - -// Makes POST request -httpResp, err := sendRequester.SendRequest(&http.Request{ - Method: "POST", - Url: config.GraphqlEndpoint, - Headers: map[string]string{ - "Content-Type": "application/json", - }, - Body: requestBody, -}).Await() -``` - -### TypeScript Implementation - -```typescript -// Uses runInNodeMode pattern with custom aggregation -const firstResultAggregation = (results: string[]) => results[0] -const result = runtime.runInNodeMode( - fetchGraphData, - firstResultAggregation -)().result() - -// GraphQL request structure -const gqlRequest: GraphQLRequest = { - query: nodeRuntime.config.query, - variables: nodeRuntime.config.variables, -} - -// Makes POST request -const resp = httpClient.sendRequest(nodeRuntime, { - url: nodeRuntime.config.graphqlEndpoint, - method: "POST" as const, - headers: { - "Content-Type": "application/json", - }, - body: new TextEncoder().encode(requestBody), -}).result() -``` - ## Setup and Testing ### Prerequisites @@ -163,12 +104,12 @@ const resp = httpClient.sendRequest(nodeRuntime, { **For Go workflow:** 1. Install CRE CLI 2. Login: `cre login` -3. Go 1.23+ installed +3. Install Go **For TypeScript workflow:** 1. Install CRE CLI 2. Login: `cre login` -3. Bun installed (or Node.js) +3. Install Bun (or Node.js) 4. Run `bun install` in the workflow directory ### Running the Workflows @@ -176,7 +117,7 @@ const resp = httpClient.sendRequest(nodeRuntime, { **Go Workflow:** ```bash cd building-blocks/indexer-fetch/indexer-fetch-go -cre workflow simulate my-workflow --target staging-settings +cre workflow simulate workflow --target staging-settings ``` **TypeScript Workflow:** @@ -185,7 +126,7 @@ cd building-blocks/indexer-fetch/indexer-fetch-ts cre workflow simulate workflow --target staging-settings ``` -### Expected Output +### Example Output Both workflows return JSON output like: @@ -212,17 +153,6 @@ Both workflows return JSON output like: } ``` -## Status - -### ✅ Both Workflows Working - -- ✅ Workflow structure and configuration -- ✅ Compilation and WASM generation -- ✅ Cron trigger setup -- ✅ GraphQL request formatting -- ✅ HTTP requests to The Graph -- ✅ Error handling -- ✅ Successful simulation and execution ## Example Use Cases @@ -259,31 +189,7 @@ Check protocol statistics every 5 minutes: } ``` -## Comparison: Go vs TypeScript - -| Feature | Go | TypeScript | -|---------|-----|------------| -| **HTTP Pattern** | `http.SendRequest` | `runInNodeMode` with `HTTPClient` | -| **Consensus** | `ConsensusIdenticalAggregation[string]()` | Custom `firstResultAggregation` | -| **Type Safety** | Compile-time with structs | Compile-time with TypeScript types | -| **Error Handling** | Go error returns | JavaScript try/catch | -| **Entry Point** | `main.go` | `main.ts` | -| **Workflow Directory** | `my-workflow/` | `workflow/` | - ## Reference Documentation - [CRE Documentation](https://docs.chain.link/cre) -- [The Graph Documentation](https://thegraph.com/docs/) -- [Cron Expression Reference](https://en.wikipedia.org/wiki/Cron) -- [CRE TypeScript SDK](https://www.npmjs.com/package/@chainlink/cre-sdk) - -## Related Patterns - -This is a **pull pattern** workflow where the workflow initiates data fetching on a schedule. For the complementary **push pattern** (event-driven workflows triggered by indexer events), see the `indexer-events` building block. - -## Learn More - -For other workflow examples: -- See `building-blocks/read-data-feeds` for reading on-chain data feeds -- See `building-blocks/kv-store` for key-value storage patterns -- Check CRE CLI help: `cre workflow simulate --help` +- [The Graph Documentation](https://thegraph.com/docs/) \ No newline at end of file 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 index 31beb99b..38010e00 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts @@ -46,7 +46,8 @@ const fetchGraphData = (nodeRuntime: NodeRuntime): string => { } // Send the request using the HTTP client - const resp = httpClient.sendRequest(nodeRuntime, req).result() + // @ts-ignore + const resp = httpClient.sendRequest(nodeRuntime, req).result() // Parse the GraphQL response const bodyText = new TextDecoder().decode(resp.body) @@ -79,7 +80,8 @@ const onIndexerCronTrigger = (runtime: Runtime): string => { // We define a simple aggregation that takes the first result since all nodes // should return identical data from The Graph. const firstResultAggregation = (results: string[]) => results[0] - const result = runtime.runInNodeMode(fetchGraphData, firstResultAggregation)().result() + // @ts-ignore + const result = runtime.runInNodeMode(fetchGraphData, firstResultAggregation)().result() runtime.log(`Indexer data fetched successfully | timestamp=${timestamp}`) From 94746029b0c59e5e5a8c3321051361a2a8f5533e Mon Sep 17 00:00:00 2001 From: mayowa Date: Wed, 26 Nov 2025 15:22:33 +0100 Subject: [PATCH 06/12] update vulnerable dependency --- building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod | 4 ++-- building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod index 9f8e3907..ebdf80cb 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod @@ -6,7 +6,6 @@ toolchain go1.24.10 require ( github.com/ethereum/go-ethereum v1.16.4 - github.com/shopspring/decimal v1.4.0 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 @@ -20,7 +19,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.0 // indirect + github.com/consensys/gnark-crypto v0.19.0 // 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 @@ -36,6 +35,7 @@ require ( 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/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 diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum index 4d59cc03..d0b7b936 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum @@ -26,8 +26,8 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP 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.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= -github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/consensys/gnark-crypto v0.19.0 h1:zXCqeY2txSaMl6G5wFpZzMWJU9HPNh8qxPnYJ1BL9vA= +github.com/consensys/gnark-crypto v0.19.0/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= 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= From 2cd60a6458e2b6e051d4928a1bf7ce137989057d Mon Sep 17 00:00:00 2001 From: mayowa Date: Thu, 27 Nov 2025 12:47:12 +0100 Subject: [PATCH 07/12] change dependencies version --- .../indexer-fetch-go/go.mod | 2 +- .../indexer-fetch-go/go.sum | 4 +- .../workflow/workflow_test.go | 200 ------------------ .../indexer-fetch-ts/workflow/main.ts | 3 +- 4 files changed, 4 insertions(+), 205 deletions(-) delete mode 100644 building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow_test.go diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod index ebdf80cb..c673fe5e 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod @@ -19,7 +19,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.19.0 // indirect + github.com/consensys/gnark-crypto v0.18.0 // 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 diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum index d0b7b936..4d59cc03 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum @@ -26,8 +26,8 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP 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.19.0 h1:zXCqeY2txSaMl6G5wFpZzMWJU9HPNh8qxPnYJ1BL9vA= -github.com/consensys/gnark-crypto v0.19.0/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/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= diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow_test.go b/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow_test.go deleted file mode 100644 index 61c0cd1d..00000000 --- a/building-blocks/indexer-data-fetch/indexer-fetch-go/workflow/workflow_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package main - -import ( - "context" - _ "embed" - "encoding/json" - "math/big" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - pb "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" - evmmock "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock" - "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" - httpmock "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http/mock" - "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" - "github.com/smartcontractkit/cre-sdk-go/cre/testutils" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/timestamppb" - - "indexer-workflow-ts/contracts/evm/src/generated/balance_reader" - "indexer-workflow-ts/contracts/evm/src/generated/ierc20" - "indexer-workflow-ts/contracts/evm/src/generated/message_emitter" -) - -var anyExecutionTime = time.Unix(1752514917, 0) - -func TestInitWorkflow(t *testing.T) { - config := makeTestConfig(t) - runtime := testutils.NewRuntime(t, testutils.Secrets{}) - - workflow, err := InitWorkflow(config, runtime.Logger(), nil) - require.NoError(t, err) - - require.Len(t, workflow, 2) // cron, log triggers - require.Equal(t, cron.Trigger(&cron.Config{}).CapabilityID(), workflow[0].CapabilityID()) -} - -func TestOnCronTrigger(t *testing.T) { - config := makeTestConfig(t) - runtime := testutils.NewRuntime(t, testutils.Secrets{ - "": {}, - }) - - // Mock HTTP client for POR data - httpMock, err := httpmock.NewClientCapability(t) - require.NoError(t, err) - httpMock.SendRequest = func(ctx context.Context, input *http.Request) (*http.Response, error) { - // Return mock POR response - porResponse := `{ - "accountName": "TrueUSD", - "totalTrust": 1000000.0, - "totalToken": 1000000.0, - "ripcord": false, - "updatedAt": "2023-01-01T00:00:00Z" - }` - return &http.Response{Body: []byte(porResponse)}, nil - } - - // Mock EVM client - chainSelector, err := config.EVMs[0].GetChainSelector() - require.NoError(t, err) - evmMock, err := evmmock.NewClientCapability(chainSelector, t) - require.NoError(t, err) - - // Set up contract mocks using generated mock contracts - evmCfg := config.EVMs[0] - - // Mock BalanceReader for fetchNativeTokenBalance - balanceReaderMock := balance_reader.NewBalanceReaderMock( - common.HexToAddress(evmCfg.BalanceReaderAddress), - evmMock, - ) - balanceReaderMock.GetNativeBalances = func(input balance_reader.GetNativeBalancesInput) ([]*big.Int, error) { - // Return mock balance for each address (same number as input addresses) - balances := make([]*big.Int, len(input.Addresses)) - for i := range input.Addresses { - balances[i] = big.NewInt(500000000000000000) // 0.5 ETH in wei - } - return balances, nil - } - - // Mock IERC20 for getTotalSupply - ierc20Mock := ierc20.NewIERC20Mock( - common.HexToAddress(evmCfg.TokenAddress), - evmMock, - ) - ierc20Mock.TotalSupply = func() (*big.Int, error) { - return big.NewInt(1000000000000000000), nil // 1 token with 18 decimals - } - - // Note: ReserveManager WriteReportFromUpdateReserves is not a read method, - // so it's handled by the EVM mock transaction system directly - evmMock.WriteReport = func(ctx context.Context, input *evm.WriteReportRequest) (*evm.WriteReportReply, error) { - return &evm.WriteReportReply{ - TxHash: common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").Bytes(), - }, nil - } - - result, err := onPORCronTrigger(config, runtime, &cron.Payload{ - ScheduledExecutionTime: timestamppb.New(anyExecutionTime), - }) - - require.NoError(t, err) - require.NotNil(t, result) - - // Check that the result contains the expected reserve value - require.Equal(t, "1000000", result) // Should match the totalToken from mock response - - // Verify expected log messages - logs := runtime.GetLogs() - assertLogContains(t, logs, `msg="fetching por"`) - assertLogContains(t, logs, `msg=ReserveInfo`) - assertLogContains(t, logs, `msg=TotalSupply`) - assertLogContains(t, logs, `msg=TotalReserveScaled`) - assertLogContains(t, logs, `msg="Native token balance"`) -} - -func TestOnLogTrigger(t *testing.T) { - config := makeTestConfig(t) - runtime := testutils.NewRuntime(t, testutils.Secrets{}) - - // Mock EVM client - chainSelector, err := config.EVMs[0].GetChainSelector() - require.NoError(t, err) - evmMock, err := evmmock.NewClientCapability(chainSelector, t) - require.NoError(t, err) - - // Mock MessageEmitter for log trigger - evmCfg := config.EVMs[0] - messageEmitterMock := message_emitter.NewMessageEmitterMock( - common.HexToAddress(evmCfg.MessageEmitterAddress), - evmMock, - ) - messageEmitterMock.GetLastMessage = func(input message_emitter.GetLastMessageInput) (string, error) { - return "Test message from contract", nil - } - - msgEmitterAbi, err := message_emitter.MessageEmitterMetaData.GetAbi() - require.NoError(t, err) - eventData, err := abi.Arguments{msgEmitterAbi.Events["MessageEmitted"].Inputs[2]}.Pack("Test message from contract") - require.NoError(t, err, "Encoding event data should not return an error") - // Create a mock log payload - mockLog := &evm.Log{ - Topics: [][]byte{ - common.HexToHash("0x1234567890123456789012345678901234567890123456789012345678901234").Bytes(), // event signature - common.HexToHash("0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd").Bytes(), // emitter address (padded) - common.HexToHash("0x000000000000000000000000000000000000000000000000000000006716eb80").Bytes(), // additional topic - }, - Data: eventData, // this is not used by the test as we pass in mockLogDecoded, but encoding here for consistency - BlockNumber: pb.NewBigIntFromInt(big.NewInt(100)), - } - - mockLogDecoded := &bindings.DecodedLog[message_emitter.MessageEmittedDecoded]{ - Log: mockLog, - Data: message_emitter.MessageEmittedDecoded{ - Emitter: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), - Message: "Test message from contract", - Timestamp: big.NewInt(100), - }, - } - - result, err := onLogTrigger(config, runtime, mockLogDecoded) - require.NoError(t, err) - require.Equal(t, "Test message from contract", result) - - // Verify expected log messages - logs := runtime.GetLogs() - assertLogContains(t, logs, `msg="Message retrieved from the contract"`) - assertLogContains(t, logs, `blockNumber=100`) -} - -//go:embed config/config.production.json -var configJson []byte - -func makeTestConfig(t *testing.T) *Config { - config := &Config{} - require.NoError(t, json.Unmarshal(configJson, config)) - return config -} - -func assertLogContains(t *testing.T, logs [][]byte, substr string) { - for _, line := range logs { - if strings.Contains(string(line), substr) { - return - } - } - t.Fatalf("Expected logs to contain substring %q, but it was not found in logs:\n%s", - substr, strings.Join(func() []string { - var logStrings []string - for _, log := range logs { - logStrings = append(logStrings, string(log)) - } - return logStrings - }(), "\n")) -} 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 index 38010e00..d32cfefa 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts @@ -80,8 +80,7 @@ const onIndexerCronTrigger = (runtime: Runtime): string => { // We define a simple aggregation that takes the first result since all nodes // should return identical data from The Graph. const firstResultAggregation = (results: string[]) => results[0] - // @ts-ignore - const result = runtime.runInNodeMode(fetchGraphData, firstResultAggregation)().result() + const result = runtime.runInNodeMode(fetchGraphData, firstResultAggregation)().result() runtime.log(`Indexer data fetched successfully | timestamp=${timestamp}`) From c4b2159d251ec25ccf3c9cd5cbb006a299d5b766 Mon Sep 17 00:00:00 2001 From: mayowa Date: Thu, 27 Nov 2025 12:54:58 +0100 Subject: [PATCH 08/12] change dependencies version --- building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod | 4 ++-- building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod index c673fe5e..4dcedaf2 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.mod @@ -11,7 +11,6 @@ require ( 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 - github.com/stretchr/testify v1.11.1 google.golang.org/protobuf v1.36.7 ) @@ -19,7 +18,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.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 @@ -36,6 +35,7 @@ require ( 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 diff --git a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum index 4d59cc03..11360409 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum +++ b/building-blocks/indexer-data-fetch/indexer-fetch-go/go.sum @@ -26,8 +26,8 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP 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.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= -github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +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= From f28cc55b0c96e459cc4a3eca74a3623680ee66cd Mon Sep 17 00:00:00 2001 From: cl-mayowa Date: Fri, 28 Nov 2025 11:37:50 +0100 Subject: [PATCH 09/12] Update building-blocks/indexer-block-trigger/block-trigger-ts/workflow/main.ts Co-authored-by: Ernest Nowacki <124677192+ernest-nowacki@users.noreply.github.com> --- .../indexer-block-trigger/block-trigger-ts/workflow/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 32f816e3..b289006d 100644 --- a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/main.ts +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/main.ts @@ -22,7 +22,7 @@ const onHttpTrigger = (runtime: Runtime, payload: HTTPPayload): string = if (!blockData.event?.data?.block) { runtime.log("Invalid webhook payload: missing block data"); - return "Error: Invalid webhook payload structure"; + throw new Error("Error: Invalid webhook payload structure"); } const block = blockData.event.data.block; From aca2b74234ae254172e932a0ddf852c70d55880f Mon Sep 17 00:00:00 2001 From: cl-mayowa Date: Fri, 28 Nov 2025 11:45:43 +0100 Subject: [PATCH 10/12] Update building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts Co-authored-by: Ernest Nowacki <124677192+ernest-nowacki@users.noreply.github.com> --- .../indexer-fetch-ts/workflow/main.ts | 163 +++++++++--------- 1 file changed, 84 insertions(+), 79 deletions(-) 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 index d32cfefa..29c6a532 100644 --- a/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts +++ b/building-blocks/indexer-data-fetch/indexer-fetch-ts/workflow/main.ts @@ -1,103 +1,108 @@ -import { cre, Runner, type NodeRuntime, type Runtime } from "@chainlink/cre-sdk" +import { + consensusIdenticalAggregation, + cre, + type HTTPSendRequester, + json, + Runner, + type Runtime, +} from '@chainlink/cre-sdk' type Config = { - schedule: string - graphqlEndpoint: string - query: string - variables?: Record + schedule: string + graphqlEndpoint: string + query: string + variables?: Record } type GraphQLRequest = { - query: string - variables?: Record + query: string + variables?: Record } type GraphQLResponse = { - data?: unknown - errors?: unknown[] + data?: unknown + errors?: unknown[] } const initWorkflow = (config: Config) => { - const cron = new cre.capabilities.CronCapability() + const cron = new cre.capabilities.CronCapability() - return [cre.handler(cron.trigger({ schedule: config.schedule }), onIndexerCronTrigger)] + return [cre.handler(cron.trigger({ schedule: config.schedule }), onIndexerCronTrigger)] } -// fetchGraphData is the function passed to the runInNodeMode helper. +// 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 = (nodeRuntime: NodeRuntime): string => { - const httpClient = new cre.capabilities.HTTPClient() - - // Prepare GraphQL request - const gqlRequest: GraphQLRequest = { - query: nodeRuntime.config.query, - variables: nodeRuntime.config.variables, - } - - const requestBody = JSON.stringify(gqlRequest) - - const req = { - url: nodeRuntime.config.graphqlEndpoint, - method: "POST" as const, - headers: { - "Content-Type": "application/json", - }, - body: new TextEncoder().encode(requestBody), - } - - // Send the request using the HTTP client - // @ts-ignore - const resp = httpClient.sendRequest(nodeRuntime, req).result() - - // Parse the GraphQL response - const bodyText = new TextDecoder().decode(resp.body) - const gqlResponse: GraphQLResponse = JSON.parse(bodyText) - - // Check for GraphQL errors - if (gqlResponse.errors && gqlResponse.errors.length > 0) { - nodeRuntime.log(`GraphQL errors: ${JSON.stringify(gqlResponse.errors)}`) - throw new Error(`GraphQL query failed: ${JSON.stringify(gqlResponse.errors)}`) - } - - if (!gqlResponse.data) { - throw new Error("No data returned from GraphQL query") - } - - nodeRuntime.log("Successfully fetched data from indexer") - - // Return the data as a JSON string - return JSON.stringify(gqlResponse.data) +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}`) - - // Use runInNodeMode to execute the offchain fetch. - // The Graph returns deterministic data across all nodes. - // We define a simple aggregation that takes the first result since all nodes - // should return identical data from The Graph. - const firstResultAggregation = (results: string[]) => results[0] - const result = runtime.runInNodeMode(fetchGraphData, firstResultAggregation)().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) + 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) + const runner = await Runner.newRunner() + await runner.run(initWorkflow) } main() + From f63d58d0d7a99c18237687dc537f92e81ec41d51 Mon Sep 17 00:00:00 2001 From: mayowa Date: Fri, 28 Nov 2025 12:13:33 +0100 Subject: [PATCH 11/12] update workflow READMEs for block trigger --- .../block-trigger-go/workflow/README.md | 54 ++++++++---- .../block-trigger-ts/workflow/README.md | 83 +++++++++---------- 2 files changed, 80 insertions(+), 57 deletions(-) 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 index ff09cf65..4e5b52d3 100644 --- a/building-blocks/indexer-block-trigger/block-trigger-go/workflow/README.md +++ b/building-blocks/indexer-block-trigger/block-trigger-go/workflow/README.md @@ -1,22 +1,48 @@ -# Blank Workflow Example +# CRE Indexer Block Trigger Workflow (Go) -This template provides a blank workflow example. It aims to give a starting point for writing a workflow from scratch and to get started with local simulation. +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. -Steps to run the example +## Features +- Uses `http.Trigger` from CRE Go SDK +- Matches transactions to watched addresses from config +- Returns formatted JSON summary of block and matched transactions -## 1. Update .env file +## Setup and Prerequisites +1. Install CRE CLI +2. Login: `cre login` +3. Install Go -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 +## 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. Simulate the workflow -Run the command from project root directory - -```bash -cre workflow simulate --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" + } + ] +} ``` -It is recommended to look into other existing examples to see how to write a workflow. You can generate then by running the `cre init` command. +## 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/README.md b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/README.md index df03f864..f9957c0c 100644 --- a/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/README.md +++ b/building-blocks/indexer-block-trigger/block-trigger-ts/workflow/README.md @@ -1,53 +1,50 @@ -# Typescript Simple Workflow Example +# CRE Indexer Block Trigger Workflow (TypeScript) -This template provides a simple Typescript workflow example. It shows how to create a simple "Hello World" workflow using 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. -Steps to run the example +## Features +- Uses HTTP trigger from CRE TypeScript SDK +- Matches transactions to watched addresses from config +- Returns formatted JSON summary of block and matched transactions -## 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. +## 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 && bun install +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: For a workflow directory named `hello-world` the command would be: - -```bash -cd hello-world && bun install +## Example Output +```json +{ + "blockNumber": 12345678, + "blockHash": "0xabc...", + "timestamp": 1700000000, + "totalLogs": 42, + "uniqueTransactions": 10, + "matchedTransactions": 2, + "transactions": [ + { + "hash": "0xdef...", + "from": "0x...", + "to": "0x73b668d8374ddb42c9e2f46fd5b754ac215495bc", + "value": "1000000000000000000" + } + ] +} ``` -## 3. Simulate the workflow - -Run the command from project root directory +## Example Use Cases +- Monitoring high-value addresses +- Contract interaction tracking +- Block-level analytics -```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 -``` +## Reference Documentation +- [CRE Documentation](https://docs.chain.link/cre) +- [Alchemy Webhooks](https://www.alchemy.com/docs/reference/custom-webhook) From 2ebd57edd4b9d1cf82741f6161efd86605da629e Mon Sep 17 00:00:00 2001 From: mayowa Date: Fri, 28 Nov 2025 13:27:22 +0100 Subject: [PATCH 12/12] update README for block trigger --- .../indexer-block-trigger/README.md | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/building-blocks/indexer-block-trigger/README.md b/building-blocks/indexer-block-trigger/README.md index 4818b9db..b693fa52 100644 --- a/building-blocks/indexer-block-trigger/README.md +++ b/building-blocks/indexer-block-trigger/README.md @@ -125,6 +125,28 @@ Both workflows return JSON output like: } ``` +## 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 @@ -150,4 +172,3 @@ Summarize block activity and matched transactions for analytics dashboards. - [CRE Documentation](https://docs.chain.link/cre) - [Alchemy Webhooks](https://www.alchemy.com/docs/reference/custom-webhook) -