In this tutorial, I am going to show you how to create your first Web3 application on Ethereum using genezio and Blast API. You don’t have to know anything beforehand to follow along. I will introduce you to the most basic blockchain concepts and tools to get you from zero to hero in Web3 development. Excited? Let’s get started 🤩
After following through with this tutorial, you’ll be able to:
- Create a wallet with Metamask
- Use Blast API to connect to the Ethereum Mainnet
- Use NodeJS and Mongo DB to build a smart contract indexer to get the events happening on a blockchain
- Use React to develop a minimalist frontend
- Deploy a full stack application on genezio
You can find the complete project on Github . Feel free to take a look at it to better understand how the complete project should look.
If you get stuck along the way or you have any questions, don’t hesitate to contact me on Discord or write me an email at andreia@genez.io. I am more than happy to help 😄
Contents
Introduction
What are you going to build?
Today, you are going to build a smart contract indexer application. This app will be able to allow users to query events related to smart contracts on various blockchain networks. For this example, I chose to show you how to connect to the Ethereum Mainnet .
Following, I am going to break down the steps you are going to get through:
- Install dependencies and set up your project
- Implement the server (backend) side of the project using NodeJS and genezio
- Implement the client (frontend) side of the project using React
- Test and deploy your application using genezio
Technical Tutorial
This step is going to introduce you to quite a few new tools for interacting with a blockchain. Don’t worry if you’ve never worked with them because I am going to walk you through every new concept.
Install genezio
To start implementing your first Web3 app, you’ll need to host it somewhere on the cloud where it can be easily accessed by your users.
You will use genezio to deploy your project quickly and easy with just a single command.
If you don’t have node and npm installed on your machine, head over to their documentation page to set them.
Install genezio using npm:
npm install genezio -g
Start using genezio by running the following command in your terminal:
genezio login
At any step, if you get stuck at any moment you can use genezio help
or genezio [command] help
to get more help with the tool.
At the end of this tutorial, the folder hierarchy of your project should be similar to this:
blockchain-project/
├── server-blockchain-project/
│ ├── genezio.yaml
│ ├── event.js
│ ├── blockchain.js
│ ├── abi.js
│ └── config.js
└── client-blockchain-project/
├── src/
│ ├── sdk/
│ └── App.js
└── build/
This is just a small spoiler to understand where things are going to fit in.
Implement the server-side
Create the folders hierarchy for the project:
mkdir blockchain-project && cd blockchain-project
To set up a new genezio project, initialize your configuration:
genezio init
genezio init
will create a genezio.yaml
file in the server-blockchain-project
directory that will contain the configuration for the project server.
After completing the wizard, your terminal should look like this:
What is the name of the project: server-blockchain-project
What region do you want to deploy your project to? [default value: us-east-1]: us-east-1
Your genezio project was successfully initialized!
The genezio.yaml configuration file was generated. You can now add the classes that you want to deploy using the 'genezio addClass <className> <classType>' command.
Change into the newly created server-blockchain-project
folder:
cd server-blockchain-project
Create a package.json
using npm:
npm init -y
Install the web3
and mongoose
npm packages by executing:
npm install web3 mongoose
These npm packages will be used to interact with the blockchain and to save the events triggered on the blockchain in a Mongo database.
Create a config.js
file in the server-blockchain-project
directory.
// server-blockchain-project/config.js
// Replace these values with your own
export const CONTRACT_ADDRESS = <todo-paste-the-contract-address-here>
export const BLAST_API_RPC_ENDPOINT = <todo-paste-the-blast-api-rpc-endpoint-here>
export const MONGO_DB_URI = <todo-paste-the-mongo-db-uri-here>
Create an abi.js
in the server-blockchain-project
directory and paste there the Ethereum bytecode of the smart contract:
// server-blockchain-project/abi.js
export const abi = <todo-paste-the-abi-here>
Expand each of the following sections to get the necessary variables (CONTRACT_ADDRESS
, abi
, BLAST_API_RPC_ENDPOINT
, MONGO_DB_URI
) if you don’t have them already.
Get a smart contract address and ABI - Application Binary Interface
Get a smart contract address and ABI (Application Binary Interface)
Head to
OpenSea
and choose the cutest NFT collection from trending.
Open the collection and select “View on EtherScan”.
EtherScan
is an analytics platform for smart contracts deployed on the Ethereum blockchain. Here you can see all of the events managed by this smart contract under the “Events” tab.
What is important on this page is:
- The contract address - think of it as a unique name for the smart contract
Copy the contract address from the main page and paste it in the
config.js
file in theserver-blockchain-project
directory.
- The ABI - the binary that the Ethereum Virtual Machine knows how to execute.
Click on the “Contract” tab and scroll down to the “Contract ABI” section. Copy it from here and paste it in the abi.js
file in the server-blockchain-project
directory.
Get access to an Ethereum endpoint using Blast API
Create a wallet with Metamask
Metamask
is a browser extension that will allow your browser to access the Ethereum blockchain. More than that, Metamask will also allow you to manage your ETH transactions. Install Metamask on your machine following the steps from their page.
Note to yourself - Remember the password you set because it will be useful in the next steps.
When you successfully finish this step, your screen should look like this:
The wallet created with Metamask will be used to connect to an Ethereum endpoint using Blast API in the next step.
Get access to an Ethereum endpoint using Blast API
A blockchain is very similar to a public network of machines. In order to connect to the network you either need to plug your machine into the network and receive an IP address of your own, or you can remotely connect to an existing machine and use it to interact with the network.
To interact with a given blockchain, you can either host and add your node to the blockchain or access an existing node. In this tutorial, we are going to use Blast API to get an endpoint to an existing node from the Ethereum Mainnet.
Head over to the
Blast API webpage
and click on “Get Endpoint” - this will allow you to interact with the blockchain using an existing node.
From there, select “Create a Consumer App” and connect using your Metamask wallet. Follow through the pop-ups that are appearing on your screen.
Now you can set up a project to get access to an Ethereum endpoint. Select “Add a new project”.
Enter a project name and select the “development” environment. After you are happy with the name, hit the “Create project” button.
A dashboard with your active endpoints will appear. For now, there is no endpoint you are connected to, so let’s change that. Head to the “Available Endpoints” tab and select “Ethereum” and, then, “Ethereum Mainnet”.
Congrats 🥳 Now you are connected to an Ethereum node.
To be able to interact with the node in your application, click on the_ “Active Endpoints”_, on the “Ethereum” widget and copy the RPC endpoint and paste it in the config.js
file in the server-blockchain-project
directory.
Create a Mongo Database
Create a Mongo Database
You will need to set up a Mongo Database to store the events that are triggered on the smart contract. Follow the steps from this tutorial to create a free Mongo Database on
MongoDB Atlas
and integrate it within your genezio project.
After you get a Mongo Database URI, add it to the config.js
file in the server-blockchain-project
directory.
Create a Mongo database model to save the events triggered on the smart contract.
Inside a file at the path server-blockchain-project/event.js
add the following code snippet:
// server-blockchain-project/event.js
import mongoose from 'mongoose'
const eventSchema = new mongoose.Schema({
id: String,
name: String,
parameters: Map,
blockNumber: Number,
logIndex: Number,
});
export const EventModel = mongoose.models.Event || mongoose.model('Event', eventSchema);
Create a blockchain.js
file in the server-blockchain-project
directory with a class name BlockchainServer
. The class will have a constructor that will initialize the services used by the application - the endpoint to the Ethereum Blockchain using Blast API and it will set up the smart contract of your selected NFT.
// server-blockchain-project/blockchain.js
import Web3 from "web3"
import { mongoose } from "mongoose";
import { abi } from "./abi.js";
import { CONTRACT_ADDRESS, BLAST_API_RPC_ENDPOINT, MONGO_DB_URI} from "./config.js";
/**
* The Blockchain server class that will be deployed on the genezio infrastructure.
*/
export class BlockchainServer {
constructor() {
mongoose.connect(MONGO_DB_URI);
this.web3 = new Web3(BLAST_API_RPC_ENDPOINT);
this.contract = new this.web3.eth.Contract(JSON.parse(abi), CONTRACT_ADDRESS);
this.knownEventTokens = this.contract.options.jsonInterface.filter((token) => {
return token.type === 'event';
});
}
}
Add a method in the class BlockchainServer
to decode events from the blockchain. This method is going to be private.
/**
* Private method that decodes an event and returns the name and the parameters.
*
* This will not be callable using the genezio SDK. Only the public methods are exposed publicly.
*
* @param {*} event
* @returns An object containing the name of the event and its parameters.
*/
#decodeEvent(event) {
// Retrieve the event declaration from the ABI
const eventToken = this.knownEventTokens.find((knownEventToken) => {
return knownEventToken.signature === event.topics[0];
});
if (!eventToken) {
console.log('cannot process log %d', event.logIndex);
return undefined;
}
// Decode the event
const decodedEvent = this.web3.eth.abi.decodeLog(
eventToken.inputs,
event.data,
event.topics.slice(1),
)
// Parse the parameters in a simple dictionary
const parameters = {}
eventToken.inputs.forEach((input) => {
parameters[input.name] = decodedEvent[input.name]
})
return {
name: eventToken.name,
parameters,
}
}
Add a method that will sync the events triggered on the smart contract and index them in our database.
This method will be called periodically by configuring a scheduled expression (cron) in genezio.yaml
.
/**
* Method that will be called periodically by the genezio infrastructure to index the events.
*
* The frequency with which the method will be called can be modified from the genezio YAML file.
*
*/
async sync() {
// Get the current block number and request the last 100 blocks
const blockNumber = await this.web3.eth.getBlockNumber()
let events = await this.web3.eth.getPastLogs({ address: CONTRACT_ADDRESS, fromBlock: blockNumber - 50, toBlock: blockNumber });
console.log(`New sync started with ${events.length} to save`)
for (const event of events) {
const decodedEvent = this.#decodeEvent(event)
if (!decodedEvent) {
continue
}
// Insert the missing events.
await EventModel.findOneAndUpdate({ id: `${event.transactionHash}-${event.logIndex}` }, {
$setOnInsert: {
id: `${event.transactionHash}-${event.logIndex}`,
name: decodedEvent.name,
parameters: decodedEvent.parameters,
blockNumber: event.blockNumber,
logIndex: event.logIndex
}
}, { upsert: true });
}
}
Lastly, add a method to get the saved events from the database.
/**
* Method used to get all the events in a paginated way.
*
* The method will be part of the genezio SDK.
*
* @param {*} from The starting index of the first event.
* @param {*} limit The number of events that will be part of the response.
* @returns
*/
async GetEvents(from, limit) {
console.log(`Received getEvents request with from = ${from} limit = ${limit}`)
const count = await EventModel.count()
console.log("Event count", count)
const events = await EventModel.find(undefined, undefined, { skip: from, limit, sort: { "blockNumber": -1, "logIndex": -1 } })
return {
count,
"events": events.map((event) => ({ id: event.id, name: event.name, parameters: event.parameters, blockNumber: event.blockNumber }))
}
}
Add blockchain.js
to the genezio configuration file to prepare this class for deployment.
genezio addClass blockchain.js
Configure the sync()
method in the genezio.yaml
to be a cron that triggers every minute.
The file genezio.yaml
file should look like this:
name: blockchain-project
region: us-east-1
sdk:
language: js
path: ../client-blockchain-project/src/sdk/
classes:
- path: ./blockchain.js
type: jsonrpc
methods:
- name: sync
type: cron
cronString: "* * * * *"
Implement the client-side
In this section you’ll build the client side of the project. This is going to be a simple React app.
Create a client-blockchain-project
directory in the root directory of your project where you’ll build the frontend application.
cd ../
mkdir client-blockchain-project
cd client-blockchain-project
Create a new React app with the following command:
npx create-react-app .
Install the following packages to get Material React UI components:
npm install @mui/material @emotion/react @emotion/styled
Modify src/App.js
as follows:
import { useEffect, useState } from "react";
import Pagination from '@mui/material/Pagination';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import './App.css';
import { BlockchainServer } from "./sdk/blockchainServer.sdk"
const CHUNKS = 10
function App() {
const [totalCount, setTotalCount] = useState(0)
const [currentIndex, setCurrentIndex] = useState(0)
const [events, setEvents] = useState([])
useEffect(() => {
setCurrentIndex(0)
}, [])
useEffect(() => {
// Use the SDK to get events from the BlockchainServer class hosted on genezio
BlockchainServer.GetEvents(currentIndex, CHUNKS)
.then((response) => {
setEvents(response.events)
setTotalCount(response.count)
})
.catch((error) => {
console.error("An error occurred!", error)
setEvents([])
})
}, [currentIndex])
const handleChange = (param, value) => {
setCurrentIndex((value - 1) * CHUNKS)
}
return (
<div className="App">
<header className="App-header">
<h1>Genezio Smart Contract Indexer</h1>
<List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
{events.map((event) =>
<ListItem>
<ListItemText primary={event.name} secondary={event.blockNumber + " " + JSON.stringify(event.parameters)} />
</ListItem>
)
}
</List>
<div>
<Pagination count={Math.floor(totalCount / CHUNKS)} onChange={handleChange}></Pagination>
</div>
</header>
</div>
);
}
export default App;
Test your project locally
It is recommended to test your project locally before deploying it to make sure that everything works as expected. Execute the following commands to change the directory and start a local testing process:
Make sure that you are in the server-blockchain-project
directory:
cd ../server-blockchain-project
Run the following command to start a local testing process:
genezio local
Your screen should look like this:
Generating your SDK...✅
Your code was deployed and the SDK was successfully generated!
Test your code at https://app.genez.io/test-interface/local?port=8083
Server listening on port 8083
Right now the server side of your project is listening to port 8083. You can head to app.genez.io/test-interface/local?port=8083 to interact with it from the GUI.
You can also test the client side of your project. Do not stop the genezio local
process. Open up a new terminal and head over to the client-blockchain-project
directory. There you can execute the following commands:
npm start
You can now interact with your project from the browser at http://localhost:3000 .
Deploy your project
Prepare the configuration for your server before deploying it.
Your server-blockchain-project/genezio.yaml
file should look like this:
name: blockchain-project
region: us-east-1
sdk:
language: js
path: ../client-blockchain-project/src/sdk/
scripts:
preFrontendDeploy: cd ../client-blockchain-project && npm install && npm run build
frontend:
path: ../client-blockchain-project/build
classes:
- path: ./blockchain.js
type: jsonrpc
methods:
- name: sync
type: cron
cronString: "* * * * *"
To deploy both the backend and frontend of your app, go to the server-blockchain-project
directory and deploy it with genezio. Hang in there because it might take a while.
cd server-blockchain-project/
genezio deploy
After the project has been successfully deployed, your terminal should look like this:
Deploying your backend project to genezio infrastructure...
Bundling your code and uploading it...✅
Checking your credentials...✅
Doing the final touch-ups...✅
Your code was deployed and the SDK was successfully generated!
Your backend project has been deployed and is available at https://app.genez.io/project/<project-id-here>
Deploying your frontend to genezio infrastructure...
No subdomain specified in the genezio.yaml configuration file. We will provide a random one for you.
Frontend successfully deployed at https://cyan-light-owl.app.genez.io.
Congrats again 🥳 Now you can manage your project from the genezio Dashboard - you can test it, check the logs, or delete it if you don’t need it anymore.
I hope you enjoyed this tutorial and I encourage you to check out our other tutorials for more tips and tricks on developing your ninja developer skills 🥷 💻