Build a Custom Blockchain API
Introduction
In the previous hacks, you built some pretty complex smart contracts. But building a robust UI/UX around these smart contracts and their on-chain data can be challenging due to the nature of blockchains.
Let's take our Build a Decentralized Grants Program hack as an example. In this project, anyone can propose a grant, and these proposals are voted on by token holders. The details of each proposal, including the title, description, and the proposal contract, are stored on the blockchain.
However, directly querying the blockchain for this data every time you want to display it in your application's UI can be slow and inefficient. And that information might not always be formatted and organized in a way that makes sense for your use case.
This is where Chainhook comes in. Chainhook allows you to listen for specific on-chain events, such as the submission of a new grant proposal, and trigger actions in response - like inserting specific data into a database to query off-chain.
In this guide, we'll walk you through the process of building a custom API with Chainhook using the Hiro Platform, with a focus on integrating it with the contracts you built for the decentralized grants program hack.
Note
You're welcome to use any Clarity app for your submission; you do not have to build with the contracts from the decentralized grants program hack. The goal is to show you how to use Chainhook to enhance the functionality and user experience of your decentralized applications.
Creating a Server
In this section, we'll briefly discuss setting up an Express server to handle incoming event data from Chainhook. While a detailed walkthrough of setting up an Express server is beyond the scope of this guide, we provide an example app that you can reference from here.
To illustrate how we can handle this event data, here is an example server.ts
file:
const express = require('express');
const app = express();
app.use(express.json());
app.post("/api/events", (req, res) => {
const events = req.body;
// Process the event data here
res.status(200).send({ message: "Event received" });
});
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
In this example, the server listens for POST requests at the /api/events
route. When a request is received from our Chainhook (which we will set up in the next section), we can extract the event data from the request body and process it.
Parsing the Event Data
The Chainhook payload comes with a lot of information, so let's break down some of the more important parts inside of the req.body
for this guide.
Here is a truncated look at the payload structure from Chainhook:
{
"apply": [
{
"transactions": [
{
"metadata": {
"receipt": "..."
},
"operations": [
{
"...": "..."
}
]
}
]
}
]
}
Each item in the apply
array should contain a transactions
array. Each transaction
object should have a metadata
object with a receipt
attribute and an operations
array.
-
transaction.metadata.receipt
: This attribute contains information about any events that were triggered from the function. -
transaction.operations
: This is an array that contains more specific information about what happened as a result of the function call. For instance if a token transfer occured, each object inside this array would contain various attributes and information regarding the account, amount, type, and status of the token transfer.
Here's what your POST route might look like after filtering for the relevant data in your server.ts
file:
app.post("/api/events", async (req, res) => {
const events = req.body;
// Loop through each item in the apply array
events.apply.forEach((item: any) => {
// Loop through each transaction in the item
item.transactions.forEach((transaction: any) => {
// If the transaction has operations, loop through them
if (transaction.operations) {
transaction.operations.forEach((operation: any) => {
// Log the operation
console.log({ operation });
});
}
});
});
// Send a response back to Chainhook to acknowledge receipt of the event
res.status(200).send({ message: "Proposal added!" });
});
For the next section, you will need to expose your local server via https
so that Chainhook can deliver the payload
. To do that, run the following command:
npx localtunnel --port <your-port-number>
Note
There are several tools for exposing localhost to the world so choose the one you prefer, for more information on
localtunnel
you can click here.
Integrating With Chainhook
In this section, we'll guide you through the process of integrating Chainhook with your smart contracts. This involves creating a Chainhook predicate that matches the specific on-chain events you want to respond to. We'll be using the Hiro Platform for this process, which provides a user-friendly interface for creating and managing your Chainhooks. By the end of this section, you'll have a Chainhook set up and ready to trigger actions in response to on-chain events.
Deploying Your Contracts to Testnet
Before we begin, make sure you have logged in to the Hiro Platform and created or imported a project. If you need help with this, refer to the Create Project guide.
In order to create our chainhooks, we need to deploy our contracts to testnet
. From your projects page, click on the Deploy button near the top right of your screen. You'll see your deployment options: let's choose Generate for Testnet.
Note
The Platform will provide a step for requesting Testnet STX, but you can also go straight to the Stacks Testnet Faucet. Once you have testnet STX tokens, you can deploy your contracts to testnet using the same methods you used for devnet, but with the network parameter set to testnet.
Creating a Chainhook
Let's create our first Chainhook. We're going to start by creating a predicate that matches the propose
function in your proposal-submission
contract. This function is called when a new grant proposal is submitted.
Here's an example of how you might define this predicate.json
file for your contract:
{
"chain": "stacks",
"uuid": "1",
"name": "New Grant Proposal",
"version": 1,
"networks": {
"testnet": {
"if_this": {
"scope": "contract_call",
"contract_identifier": "ST2BSV94A650WGZ2YZ5Y8HM93W01NGT4GY0MGJECG.proposal-submission",
"method": "propose"
},
"then_that": {
"http_post": {
"url": "<your-https-server-url>",
"authorization_header": "Bearer 12345"
}
},
"start_block": 138339
}
}
}
Note
Make sure to swap out the details in your
predicate.json
file to match your contracts ABI
The if_this
section specifies the conditions for the events that this Chainhook will respond to. In this case, it's looking for calls to the propose
method in the proposal-submission
contract on the Stacks testnet.
The then_that
section specifies what action the Chainhook should take when it detects an event that matches the if_this
conditions. Here, it's set up to send a POST request to your specified URL with the event data.
The start_block
field is used to specify the starting block for the Chainhook to start listening from. This is useful for ignoring blocks that were mined before the Chainhook was set up.
Once you have created a similar file for your specific contract. select the Chainhooks tab inside your project and upload your predicate file using the Upload Chainhook option. For more information, you can follow the Create Chainhooks section.
Testing Your Chainhook
Once you've successfully uploaded your Chainhook predicate, it's time to test it out. You can do this by performing an action that aligns with your if_this
- then_that
logic.
For instance, if your Chainhook is set up to respond to a specific contract call, you can trigger that call and then check your /api/post
endpoint. If everything is set up correctly, you should see any information you've requested to be logged.
The next section presents several challenges that will help you put the finishing touches on your API. With the foundation you've built, you're well-equipped to take on these challenges and continue your journey in building robust, decentralized applications.
Congratulations on reaching this point! You now have all the building blocks needed to create a custom API using Chainhook and integrate it with your smart contracts. This is a significant step towards enhancing the functionality and user experience of your decentralized applications.
Note
This is a crucial step in verifying that your Chainhook is working as expected. If you don't see the expected output, you may need to revisit your predicate and ensure it's correctly configured.
Challenges
The following challenges are additional features you can implement to further differentiate your project. Feel free to add any other features you want.
Starter
Create More Than 1 Chainhook: Don't stop at just 1! Follow the process above to create more than 1 chainhook for your dApp and parse the data accordingly.
Intermediate
Create Consumer-Facing API Endpoints: Up until now, we've shown you how to capture and process the initial payload
data sent from a chainhook. However, to provide a robust user experience, you'll want to do more than just output raw data. The challenge here is to create additional API endpoints that present the filtered and extracted data from the initial /api/events
payload in a consumer-friendly format. This could involve adding a database layer for organizing the data, adding additional context, or transforming the data to match the needs of your application's frontend. For a quick db setup, you can try using something like Supabase.
// Example using Supabase
const supabase = createClient("https://<project>.supabase.co", "<your-anon-key>");
app.post("/api/events", async (req, res) => {
// ...
item.transactions.forEach((transaction: any) => {
if (transaction.operations) {
transaction.operations.forEach((operation: any) => {
const data = operation // Parse out the data you want
// Insert and save it to db
const { error } = await supabase
.from('proposals')
.insert(data)
});
}
});
// ...
});
Advanced
UI Integration: The final challenge is to integrate your consumer-facing API into your frontend. This involves taking the data you've processed and organized in your API and displaying it in a meaningful and user-friendly way in your application. This could involve creating dynamic components that update in response to new data, adding interactive elements that allow users to explore the data, or incorporating visualizations to help users understand the data.
Prizes and Submissions
We are giving 300K sats each to the 10 best submissions for this hack. If you've participated in the last 2 hacks and complete this one too, we will also be giving a grand prize of 3M sats to the dev who submits the best and most consistent projects across all 3 hacks.
In order to be eligible for both of these prizes, you must complete the hack and submit before the deadline. The deadline for this hack is next Wednesday, December 20th at midnight ET.
To submit your project, please use this form. Good luck!
View the official Hiro Hacks rules here.