Skip to main content

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

  1. Inside your project root directory, create a new folder that will hold our express backend.
mkdir backend
cd backend
  1. Install the required dependencies.
npm install moralis express cors dotenv nodemon
  1. Create a new file with a basic express app insidebackend/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}`)
})
  1. 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"
}
}
  1. 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
  1. 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.

  1. 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);
}
});
  1. 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);
}
});
  1. 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);
}
});
  1. 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 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.

  1. Go back to your root directory and create a React application.
npx create-react-app frontend
  1. Go inside the frontend directory using cd frontend and install the required dependencies.
npm install axios @web3uikit/core @web3uikit/web3 @web3uikit/icons
  1. Inside thesrc 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.

  1. 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;
  1. 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;
  1. 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;
  1. 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;
  1. 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">
Tansfer 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;
  1. 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.

  1. 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;
}
  1. 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.

  1. Run the start script inside the frontend directory.
npm run start
  1. You can now access <http://localhost:3000/> and should see your app running.

Youtube Video