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.