Zapper Clone
Introduction
This tutorial teaches you how to build a Zapper-like application where you can check Token Balance, Transaction History and NFT balances.
Prerequisites
Before getting started, make sure you have the following ready:
- NodeJS v14+
- A package manager: NPM, Yarn, PNPM
Step 1: Backend setup
Initial setup
- Inside your project root directory, create a new folder that will hold our express backend.
mkdir backend
cd backend
- Install the required dependencies.
- npm
- Yarn
- pnpm
npm install moralis express cors dotenv nodemon
yarn add moralis express cors dotenv nodemon
pnpm add moralis express cors dotenv nodemon
- Create a new file with a basic express app inside
backend/index.js
const express = require('express')
const cors = require('cors')
const app = express()
const port = 8080
app.use(cors())
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
- Add our start script inside
backend/package.json
{
"name": "zapper",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.0.2",
"express": "^4.18.1",
"moralis": "^2.4.0",
"nodemon": "^2.0.19"
}
}
- Create a
.env
file and add your Moralis API Key inside. You can get your own key following this tutorial How to get an API Key.
MORALIS_API_KEY = YOUR_KEY_HERE
- Now in your
backend/index.js
you can add Moralis.
const Moralis = require("moralis").default;
const express = require("express");
const cors = require("cors");
require("dotenv").config();
const app = express();
const port = 8080;
app.use(cors());
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
Setup our custom endpoints.
We can now start adding our endpoints which will be called from our frontend app.
- Get the native balance.
//GET AMOUNT AND VALUE OF NATIVE TOKENS
app.get("/nativeBalance", async (req, res) => {
await Moralis.start({ apiKey: process.env.MORALIS_API_KEY });
try {
const { address, chain } = req.query;
const response = await Moralis.EvmApi.balance.getNativeBalance({
address: address,
chain: chain,
});
const nativeBalance = response.data;
let nativeCurrency;
if (chain === "0x1") {
nativeCurrency = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
} else if (chain === "0x89") {
nativeCurrency = "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270";
}
const nativePrice = await Moralis.EvmApi.token.getTokenPrice({
address: nativeCurrency, //WETH Contract
chain: chain,
});
nativeBalance.usd = nativePrice.data.usdPrice;
res.send(nativeBalance);
} catch (e) {
res.send(e);
}
});
- Get ERC20 balances and prices.
//GET AMOUNT AND VALUE OF ERC20 TOKENS
app.get("/tokenBalances", async (req, res) => {
await Moralis.start({ apiKey: process.env.MORALIS_API_KEY });
try {
const { address, chain } = req.query;
const response = await Moralis.EvmApi.token.getWalletTokenBalances({
address: address,
chain: chain,
});
let tokens = response.data;
let legitTokens = [];
for (let i = 0; i < tokens.length; i++) {
try {
const priceResponse = await Moralis.EvmApi.token.getTokenPrice({
address: tokens[i].token_address,
chain: chain,
});
if (priceResponse.data.usdPrice > 0.01) {
tokens[i].usd = priceResponse.data.usdPrice;
legitTokens.push(tokens[i]);
} else {
console.log("💩 coin");
}
} catch (e) {
console.log(e);
}
}
res.send(legitTokens);
} catch (e) {
res.send(e);
}
});
- Get NFTs.
//GET Users NFT's
app.get("/nftBalance", async (req, res) => {
await Moralis.start({ apiKey: process.env.MORALIS_API_KEY });
try {
const { address, chain } = req.query;
const response = await Moralis.EvmApi.nft.getWalletNFTs({
address: address,
chain: chain,
});
const userNFTs = response.data;
res.send(userNFTs);
} catch (e) {
res.send(e);
}
});
- Get historical ERC20 transfers.
//GET USERS TOKEN TRANSFERS
app.get("/tokenTransfers", async (req, res) => {
await Moralis.start({ apiKey: process.env.MORALIS_API_KEY });
try {
const { address, chain } = req.query;
const response = await Moralis.EvmApi.token.getWalletTokenTransfers({
address: address,
chain: chain,
});
const userTrans = response.data.result;
let userTransDetails = [];
for (let i = 0; i < userTrans.length; i++) {
try {
const metaResponse = await Moralis.EvmApi.token.getTokenMetadata({
addresses: [userTrans[i].address],
chain: chain,
});
if (metaResponse.data) {
userTrans[i].decimals = metaResponse.data[0].decimals;
userTrans[i].symbol = metaResponse.data[0].symbol;
userTransDetails.push(userTrans[i]);
} else {
console.log("no details for coin");
}
} catch (e) {
console.log(e);
}
}
res.send(userTransDetails);
} catch (e) {
res.send(e);
}
});
Start the express app.
- npm
- Yarn
- pnpm
npm run start
yarn run start
pnpm run start
Step 2: Frontend setup
React app setup
We will set up a react frontend and start building our UI.
To style our UI we will be using Web3uikit.
- Go back to your root directory and create a React application.
- npx
- Yarn
npx create-react-app frontend
yarn create react-app frontend
- Go inside the frontend directory using
cd frontend
and install the required dependencies.
- npm
- Yarn
- pnpm
npm install axios @web3uikit/core @web3uikit/web3 @web3uikit/icons
yarn add axios @web3uikit/core @web3uikit/web3 @web3uikit/icons
pnpm add axios @web3uikit/core @web3uikit/web3 @web3uikit/icons
- Inside the
src
directory, create a new folder that will hold React components. We will call it components.
Creating custom components
Inside the components folder, we will create multiple components which will be used inside our React app.
- NativeTokens.js
import React from "react";
import axios from "axios";
import { Table } from "@web3uikit/core";
import {Reload} from '@web3uikit/icons'
function NativeTokens({
wallet,
chain,
nativeBalance,
setNativeBalance,
nativeValue,
setNativeValue,
}) {
async function getNativeBalance() {
const response = await axios.get("http://localhost:8080/nativeBalance", {
params: {
address: wallet,
chain: chain,
},
});
if (response.data.balance && response.data.usd) {
setNativeBalance((Number(response.data.balance) / 1e18).toFixed(3));
setNativeValue(
(
(Number(response.data.balance) / 1e18) *
Number(response.data.usd)
).toFixed(2)
);
}
}
return (
<>
<div className="tabHeading">Native Balance <Reload onClick={getNativeBalance}/></div>
{(nativeBalance >0 && nativeValue >0) &&
<Table
pageSize={1}
noPagination={true}
style={{width:"900px"}}
columnsConfig="300px 300px 250px"
data={[["Native", nativeBalance, `$${nativeValue}`]]}
header={[
<span>Currency</span>,
<span>Balance</span>,
<span>Value</span>,
]}
/>
}
</>
);
}
export default NativeTokens;
- Nfts.js
import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
import { Reload } from "@web3uikit/icons";
import { Input } from "@web3uikit/core"
function Nfts({ chain, wallet, filteredNfts, setFilteredNfts, nfts, setNfts }) {
const [nameFilter, setNameFilter] = useState("");
const [idFilter, setIdFilter] = useState("");
async function getUserNfts() {
const response = await axios.get("http://localhost:8080/nftBalance", {
params: {
address: wallet,
chain: chain,
},
});
if (response.data.result) {
nftProcessing(response.data.result);
}
}
function nftProcessing(t) {
for (let i = 0; i < t.length; i++) {
let meta = JSON.parse(t[i].metadata);
if (meta && meta.image) {
if (meta.image.includes(".")) {
t[i].image = meta.image;
} else {
t[i].image = "https://ipfs.moralis.io:2053/ipfs/" + meta.image;
}
}
}
setNfts(t);
setFilteredNfts(t);
}
useEffect(() => {
if (idFilter === "" && nameFilter === "") {
return setFilteredNfts(nfts);
}
let filNfts = [];
for (let i = 0; i < nfts.length; i++) {
if (
nfts[i].name.toLowerCase().includes(nameFilter) &&
idFilter.length === 0
) {
filNfts.push(nfts[i]);
} else if (
nfts[i].token_id.includes(idFilter) &&
nameFilter.length === 0
) {
filNfts.push(nfts[i]);
} else if (
nfts[i].token_id.includes(idFilter) &&
nfts[i].name.toLowerCase().includes(nameFilter)
) {
filNfts.push(nfts[i]);
}
}
setFilteredNfts(filNfts);
}, [nameFilter, idFilter]);
return (
<>
<div className="tabHeading">
NFT Portfolio <Reload onClick={getUserNfts} />
</div>
<div className= "filters">
<Input
id="NameF"
label="Name Filter"
labelBgColor="rgb(33, 33, 38)"
value={nameFilter}
style={{}}
onChange={(e) => setNameFilter(e.target.value)}
/>
<Input
id="IdF"
label="Id Filter"
labelBgColor="rgb(33, 33, 38)"
value={idFilter}
style={{}}
onChange={(e) => setIdFilter(e.target.value)}
/>
</div>
<div className="nftList">
{filteredNfts.length > 0 &&
filteredNfts.map((e) => {
return (
<>
<div className="nftInfo">
{e.image && <img src={e.image} width={200} />}
<div>Name: {e.name}, </div>
<div>(ID: {e.token_id.slice(0,5)})</div>
</div>
</>
);
})
}
</div>
</>
);
}
export default Nfts;
- PortfolioValue.js
import React from "react";
import { useState, useEffect } from "react";
import "../App.css";
function PortfolioValue({ tokens, nativeValue }) {
const [totalValue, setTotalValue] = useState(0);
useEffect(() => {
let val = 0;
for (let i = 0; i < tokens.length; i++) {
val = val + Number(tokens[i].val);
}
val = val + Number(nativeValue);
setTotalValue(val.toFixed(2));
}, [nativeValue, tokens]);
return (
<>
<div className="totalValue">
<h3>Portfolio Total Value</h3>
<h2>
${totalValue}
</h2>
</div>
</>
);
}
export default PortfolioValue;
- Tokens.js
import React from "react";
import axios from "axios";
import { Table } from "@web3uikit/core";
import { Reload } from "@web3uikit/icons";
function Tokens({ wallet, chain, tokens, setTokens }) {
async function getTokenBalances() {
const response = await axios.get("http://localhost:8080/tokenBalances", {
params: {
address: wallet,
chain: chain,
},
});
if (response.data) {
tokenProcessing(response.data);
}
}
function tokenProcessing(t) {
for (let i = 0; i < t.length; i++) {
t[i].bal = (Number(t[i].balance) / Number(`1E${t[i].decimals}`)).toFixed(3); //1E18
t[i].val = ((Number(t[i].balance) / Number(`1E${t[i].decimals}`)) *Number(t[i].usd)).toFixed(2);
}
setTokens(t);
}
return (
<>
<div className="tabHeading">ERC20 Tokens <Reload onClick={getTokenBalances}/></div>
{tokens.length > 0 && (
<Table
pageSize={6}
noPagination={true}
style={{ width: "900px" }}
columnsConfig="300px 300px 250px"
data={tokens.map((e) => [e.symbol, e.bal, `$${e.val}`] )}
header={[
<span>Currency</span>,
<span>Balance</span>,
<span>Value</span>,
]}
/>
)}
</>
);
}
export default Tokens;
- TransferHistory.js
import React from "react";
import axios from "axios";
import { Reload } from "@web3uikit/icons";
import { Table } from "@web3uikit/core";
function TransferHistory({ chain, wallet, transfers, setTransfers }) {
async function getTokenTransfers() {
const response = await axios.get("http://localhost:8080/tokenTransfers", {
params: {
address: wallet,
chain: chain,
},
});
if (response.data) {
setTransfers(response.data);
console.log(response.data);
}
}
return (
<>
<div className="tabHeading">
Transfer History <Reload onClick={getTokenTransfers} />
</div>
<div>
{transfers.length > 0 && (
<Table
pageSize={8}
noPagination={false}
style={{ width: "90vw" }}
columnsConfig="16vw 18vw 18vw 18vw 16vw"
data={transfers.map((e) => [
e.symbol,
(Number(e.value) / Number(`1e${e.decimals}`)).toFixed(3),
`${e.from_address.slice(0, 4)}...${e.from_address.slice(38)}`,
`${e.to_address.slice(0, 4)}...${e.to_address.slice(38)}`,
e.block_timestamp.slice(0,10),
])}
header={[
<span>Token</span>,
<span>Amount</span>,
<span>From</span>,
<span>To</span>,
<span>Date</span>,
]}
/>
)}
</div>
</>
);
}
export default TransferHistory;
- WalletInputs.js
import React from "react";
import "../App.css";
import {Input, Select, CryptoLogos} from '@web3uikit/core'
function WalletInputs({chain, wallet, setChain, setWallet}) {
return (
<>
<div className="header">
<div className="title">
<svg width="40" height="40" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><path id="logo_exterior" d="M500 250C500 111.929 388.071 0 250 0C111.929 0 0 111.929 0 250C0 388.071 111.929 500 250 500C388.071 500 500 388.071 500 250Z" fill="#784FFE"></path><path id="logo_interior" fill-rule="evenodd" clip-rule="evenodd" d="M154.338 187.869L330.605 187L288.404 250.6L388 250.118L345.792 312.652L168.382 313.787L211.25 250.633L112 250.595L154.338 187.869Z" fill="white"></path></svg>
<h1>Zapper</h1>
</div>
<div className="walletInputs">
<Input
id="Wallet"
label="Wallet Address"
labelBgColor="rgb(33, 33, 38)"
value={wallet}
style={{height: "50px"}}
onChange={(e) => setWallet(e.target.value)}
/>
<Select
defaultOptionIndex={0}
id="Chain"
onChange={(e) => setChain(e.value)}
options={[
{
id: 'eth',
label: 'Ethereum',
value: "0x1",
prefix: <CryptoLogos chain="ethereum"/>
},
{
id: 'matic',
label: 'Polygon',
value: "0x89",
prefix: <CryptoLogos chain="polygon"/>
},
]}
/>
</div>
</div>
</>
);
}
export default WalletInputs;
Import our components
Now we have to import everything inside App.js
import "./App.css";
import { useState } from "react";
import NativeTokens from "./components/NativeTokens";
import Tokens from "./components/Tokens";
import TransferHistory from "./components/TransferHistory";
import Nfts from "./components/Nfts";
import WalletInputs from "./components/WalletInputs";
import PortfolioValue from "./components/PortfolioValue";
import { Avatar, TabList, Tab } from "@web3uikit/core";
function App() {
const [wallet, setWallet] = useState("");
const [chain, setChain] = useState("0x1");
const [nativeBalance, setNativeBalance] = useState(0);
const [nativeValue, setNativeValue] = useState(0);
const [tokens, setTokens] = useState([]);
const [nfts, setNfts] = useState([]);
const [filteredNfts, setFilteredNfts] = useState([]);
const [transfers, setTransfers] = useState([]);
return (
<div className="App">
<WalletInputs
chain={chain}
setChain={setChain}
wallet={wallet}
setWallet={setWallet}
/>
<div className="content">
<div className="walletInfo">
{wallet.length === 42 && (
<>
<div>
<Avatar isRounded size={130} theme="image" />
<h2>{`${wallet.slice(0, 6)}...${wallet.slice(36)}`}</h2>
</div>
<PortfolioValue
nativeValue={nativeValue}
tokens={tokens}
/>
</>
)}
</div>
<TabList>
<Tab tabKey={1} tabName={"Tokens"}>
<NativeTokens
wallet={wallet}
chain={chain}
nativeBalance={nativeBalance}
setNativeBalance={setNativeBalance}
nativeValue={nativeValue}
setNativeValue={setNativeValue}
/>
<Tokens
wallet={wallet}
chain={chain}
tokens={tokens}
setTokens={setTokens}
/>
</Tab>
<Tab tabKey={2} tabName={"Transfers"}>
<TransferHistory
chain={chain}
wallet={wallet}
transfers={transfers}
setTransfers={setTransfers}
/>
</Tab>
<Tab tabKey={3} tabName={"NFT's"}>
<Nfts
wallet={wallet}
chain={chain}
nfts={nfts}
setNfts={setNfts}
filteredNfts={filteredNfts}
setFilteredNfts={setFilteredNfts}
/>
</Tab>
</TabList>
</div>
</div>
);
}
export default App;
Step 3: Frontend styling
We will now add the required CSS style for our app.
- Inside our
src/App.css
add the following styles.
.App {
text-align: left;
min-height: 100vh;
}
.header {
height: 80px;
width: calc(100vw - 100px);
display: flex;
justify-content: flex-start;
background-color: rgb(33, 33, 38);
border-bottom: 1px solid rgb(121, 121, 121);
padding: 5px 50px;
align-items: center;
}
.title {
display: flex;
height: 60px;
align-items: center;
gap: 25px;
}
.walletInputs {
display: flex;
justify-content: flex-end;
width: 100%;
align-items: center;
gap: 25px;
}
.content {
height: 100%;
margin: 0;
padding: 100px;
background: linear-gradient(180deg, rgba(142, 211, 182, 0.3) 0%, rgba(36,20,0,0) 50%);
}
.walletInfo {
display: flex;
justify-content: space-between;
}
.totalValue {
width: 350px;
height: 150px;
padding: 10px 30px;
border-radius: 20px;
background-color: rgba(33, 33, 38, 0.6);
display: flex;
flex-direction: column;
justify-content: center;
}
.tabHeading {
font-size: 30px;
font-weight: bold;
margin: 20px 0px;
align-items: center;
display: flex;
gap: 20px;
}
.filters {
display: flex;
gap: 20px;
margin: 30px 0px;
}
.nftInfo {
justify-content: center;
align-items: center;
display: flex;
flex-direction: column;
color: white;
gap: 5px;
background-color: rgb(42, 42, 47);
border-radius: 5px;
padding: 10px;
}
.nftList {
display: flex;
justify-content: flex-start;
gap: 40px;
flex-wrap: wrap;
}
- Inside
src/index.css
add the following:
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: rgb(33, 33, 38);
color: white;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
Step 4: Start the app
We can now start our React app and check everything we have built.
- Run the start script inside the frontend directory.
- npm
- Yarn
- pnpm
npm run start
yarn run start
pnpm run start
- You can now access
<http://localhost:3000/
> and should see your app running.