Learn how to use Chainhook to observe a function call for a voting contract.
The contract_call predicate scope is designed to target direct function calls within a smart contract. When triggered, Chainhook will return a payload with transaction data detailing the on-chain events contained in these functions.
The predicate is your main interface for querying the Chainhook data indexer. Chainhook uses this predicate to select the appropriate blockchain, network, and scope for monitoring transactions.
For the Stacks blockchain, run the following command to generate a predicate template:
Terminal
$
chainhook predicates new contract-call-chainhook.json --stacks
Note
Alternatively, Hiro Platform has an excellent UI to help you to create a predicate using a form builder or upload a json file containing your predicate.
There are 3 main components to your predicate that you need to address:
1Targeting the appropriate blockchain and network
2Defining the scope and targeting the function you want to observe
3Defining the payload destination
To begin, you need to configure the predicate to target the voting contract:
Specify testnet in the network object.
Set the start_block property to 21443.
contract-call-chainhook.json
{
"chain":"stacks",
"uuid":"1",
"name":"Contract-Call-Chainhook",
"version":1,
"networks": {
"testnet": {
"start_block":21443,
"decode_clarity_values":true,
"expire_after_occurrence":1,
// ...
}
}
}
Next, define the scope of the predicate within the if_this specification.
The contract_call scope allows Chainhook to observe blockchain data when the specified function is directly called from its contract.
The function defined in the method property of your predicate must be directly called in order for Chainhook to observe events. Calling the function from another contract or from within a different function on the same contract will not generate a payload. Below is an example of a cast-vote function that would not trigger an event.
(define-public(call-cast-vote)
(cast-vote)
)
Finally, define how Chainhook delivers the payload when it is triggered by your predicate using the then_that specification.
Using file_append, specify the path where Chainhook will post the payload data.
contract-call-chainhook.json
{
"then_that": {
"file_append": {
"path":"/tmp/events.json"
}
}
}
Note
For more details on optional configurations, other predicate elements and scopes, check out the Predicate Design and Stacks scopes pages.
With your predicate set up, you can now scan for blocks that match the contract_call scope and analyze the returned payload.
Chainhook will track events where this function is directly invoked and deliver detailed transaction data at the block level, based on your configuration.
To scan the Stacks blockchain using your predicate, run the following command, replacing /path/to/contract-call-chainhook.json with the actual path to your contract-call-chainhook.json file:
The cast-vote function records a vote by storing the address that calls it. It also logs relevant data using the print function, which can be useful for when you want to track additional on-chain events that are not part of the built-in Clarity functions.
When you examine the payload, this is the data you will look for.
The hash returned in the block_identifer object is not that block hash you
would see in Stacks Explorer, but
index_block_hash returned from the Stacks API's get
block endpoint. You can use the apply array's
metadata object to get the stacks_block_hash.
We can retrieve the stacks_block_hash by navigating to the apply arrays object metadata element. This hash will match the block hash display in the Stacks Explorer.
There is also a timestamp value returned in the apply array. This UNIX time stamp represents the initiation of the Stacks block.
contract-call-payload.json
"timestamp":1722208524
Warning
The timestamp returned in this object is not the finalized block time you
would see in the Stacks Explorer, but
block_time returned from the Stacks API's get
block endpoint.
Because Chainhook is triggered on the block level, we will receive a single response that contains data specific to each transaction that matches your predicate's contract_call scope.
To find this data, we start with the apply array element of the payload object. The single object that makes up the apply array contains a child element, the transactions array. Every transaction will be represents by a single object within this array. This transaction object contains its own children elements which can be seen in the example below.
{
"apply": [
{
"transactions": [
{
"transaction_identifier": { ... },
"metdata": { ... },
"operations": [],
}
],
}
],
"rollback": [ ... ],
"chainhook": { ... }
}
Once the transaction object is returned, we can begin examining the important data elements this object contains. The first element, transaction_identifier, includes a hash value that uniquely identifies your transaction.
Next, focus on the metadata object within your contract_call data. It's crucial to determine the success state of your transaction. Chainhook captures and reports on transactions regardless of their outcome.
Utilize the success object to assess transaction success and extract the sender of the transaction and the result returned by the contract.