Hey everyone, I will be walking you through the development of an Instagram clone using Next.js, TailwindCSS, Truffle, Web3.js, and IPFS-HTTP-CLIENT. Disclaimer: I am not focusing on the Front End styling. I will leave that up to you to style how you like :) I will primarily be focusing on the backend / Solidity.
Setting up Next.js Project with TailwindCSS Template
To begin, create a new Next.js Project with TailwindCSS. To easily set this up, open your terminal and enter the command
npx create-next-app@latest -e with-tailwindcss instagram-polygon
Installing the necessary packages
Once the project has been initialized, follow these commands to get all the packages set up.
change into the project directory
cd instagram-polygon
install the required packages
yarn add web3 truffle ipfs-http-client@33.0.1
create a folder named backend
in the backend folder, run the command
npx truffle init
to set up the Ethereum development environment and the necessary folders for contracts, migrations, and build.
Congrats! You have now set up the base of your Ethereum development folder.
Truffle Setup
To use Ganache as your own personal blockchain, please visit
https://trufflesuite.com/ganache/
and download and run Ganache.
Since we are using Truffle for development, I will only include the development network as we will add the Polygon configurations before deployment :)
In the backend folder, you should now see a file named truffle-config.js. Replace the contents of the file with the following:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*",
},
},
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
version: "0.8.15",
},
},
};
This allows us to use Ganache as our testing development. The compiler section of this file also indicates that we will be using Solidity version 0.8.15 for this project.
Now that you have your configuration file set up, we can begin working on the Smart Contract for this dApp.
Setting up the smart contracts
In the backend/contracts folder, create a new file named Migrations.sol. This file is used for Truffle to read the migration whenever we want to update or change any information of the smart contract.
In this file, add the following:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
contract Migrations {
address public owner = msg.sender;
uint256 public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint256 completed) public restricted {
last_completed_migration = completed;
}
}
Now that we have our migrations contract set up, create a new file in the same backend/contracts folder named Instagram.sol. This smart contract is where we define what we want the application to do. We will be using the following structure:
- A public integer postCount that tracks the total amount of posts
- A mapping of the posts, which allows us to sort by id
- Structs of the Post which define the necessary inputs/parameters You can see this through the finished smart contract, which looks like:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract Instagram{
string public name = "Instagram";
uint256 public postCount= 0;
mapping(uint256 => Post) public posts;
struct Post{
uint256 id;
string hash;
string description;
address payable author;
}
event PostCreated(
uint256 id,
string hash,
string description,
address payable author
);
constructor() {
name = "Instagram";
}
function uploadPost(string memory _postHash, string memory _description)
public
{
// Make sure the post hash exists
require(bytes(_postHash).length > 0);
// Make sure post description exists
require(bytes(_description).length > 0);
// Make sure uploader address exists
require(msg.sender != address(0x0));
// Increment post id
postCount++;
// Add post to contract
posts[postCount] = Post(
postCount,
_postHash,
_description,
payable(msg.sender)
);
// Trigger an event
emit PostCreated(
postCount,
_postHash,
_description,
payable(msg.sender)
);
}
}
Congrats! You now have a Smart Contract set up for this dApp.
Continuing, head over to the backend/migrations folder and create a file named 1_initial_migrations.js. This file is used for the first migration to Truffle. Your file should look like this:
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
Create another file in backend/migrations named 2_deploy.js. This file contains the artifacts for the smart contract we just created. Your file should look like this:
const Instagram = artifacts.require("Instagram");
module.exports = function (deployer) {
deployer.deploy(Instagram);
};
Once you have the files set up, open a terminal and cd into the backend directory using cd/backend
Run the command npx truffle migrate --reset
to deploy the contract onto the development network.
You have now officially set up the backend for the Instagram dApp!!!
Setting up the front end connection to the smart contract
In the pages folder of the project, you should see _app.tsx and index.tsx. I personally do not enjoy using TypeScript so I change the files to .js and get rid of the :NextPage and :AppProps. This should have no negative effect to your project.
In index.js, import the following packages at the top of your file.
import Head from "next/head";
import { useEffect, useState } from "react";
import Web3 from "web3";
import Instagram from "../backend/build/contracts/Instagram.json";
We will not be using much, only a couple necessary packages to get up and running.
After importing the packages, we can start setting up the states for each function/component. Using useState, your file should contain the following just under the const Home statement.
const [account, setAccount] = useState("");
const [web3, setWeb3] = useState(null);
const [instagram, setInstagram] = useState(null);
const [postCount, setPostCount] = useState(0);
const [posts, setPosts] = useState([]);
const [buffer, setBuffer] = useState(null);
const [loading, setLoading] = useState(false);
This sets up the states for everything we need in our project.
Now we can create the functions to enable us to connect to the blockchain. Still in the index.js file, create a function called Web3Handler. This enables us to connect to MetaMask. The function looks like this:
const Web3Handler = async () => {
try {
const account = await window.ethereum.request({
method: "eth_requestAccounts",
});
const web3 = new Web3(window.ethereum);
setAccount(account[0]);
setWeb3(web3);
} catch (error) {
console.log(error);
}
};
Once you have this, we will now set up the loadBlockchainData that allows us to use the contracts information. Here we set up a statement that alerts the user if they are not connected to the proper network. This function should look like this:
const loadBlockchainData = async () => {
const web3 = new Web3(window.ethereum);
const accounts = await web3.eth.getAccounts();
setAccount(accounts[0]);
const networkId = await web3.eth.net.getId();
const networkData = Instagram.networks[networkId];
if (networkData) {
const instagram= new web3.eth.Contract(Instagram.abi, networkData.address);
setInstagram(instagram);
const postCount= await instagram.methods.postCount().call();
setPostCount(postCount);
for (let i = postCount; i >= 1; i--) {
const post = await instagram.methods.posts(i).call();
setPosts((prevPosts) => [...prevPosts, post]);
}
} else {
window.alert("Twitter contract not deployed to detected network.");
}
};
You should now have the 2 functions for the blockchain connection.
Add the following useEffect to ensure the account only gets rendered on component load.
useEffect(() => {
loadBlockchainData();
}, [account]);
We will now create the ipfs-http-client handler.
Head over to https://www.infura.io and create an account / log in. Create a new project under IPFS, and name it whatever you would like. On this screen, you will find the projectId and projectSecret that is necessary for uploading files to IPFS.
Your index.js should contain the following:
const ipfsClient = require("ipfs-http-client");
const projectId = "21sdaf....";
const projectSecret = "22fsa...";
const auth =
"Basic " + Buffer.from(projectId + ":" + projectSecret).toString("base64");
const client = ipfsClient({
host: "ipfs.infura.io",
port: 5001,
protocol: "https",
headers: {
authorization: auth,
},
});
This properly sets up the connection between IPFS, Infura, and your project.
We can now create the 2 functions that deal with uploading your post to the blockchain.
First we create the function captureFile. This function handles the attachment of the file. This should look like:
const captureFile = (event) => {
event.preventDefault();
const file = event.target.files[0];
const reader = new window.FileReader();
reader.readAsArrayBuffer(file);
reader.onloadend = () => {
setBuffer(Buffer(reader.result));
console.log("buffer", buffer);
};
};
We also have to create a function that uploads the post, uploadPost. This should look like:
const uploadPost = async (description) => {
setLoading(true);
try {
client.add(buffer, (error, result) => {
console.log("IPFS result", result);
if (error) {
console.error(error);
return;
}
setLoading(true);
instagram.methods
.uploadPost(result[0].hash, description)
.send({ from: account })
.on("transactionHash", (hash) => {
setLoading(false);
});
});
} catch (error) {
console.log(error);
setLoading(false);
}
};
You now have all the functions to get this dApp working!
Your final index.js file should look like this:
import Head from "next/head";
import { useEffect, useState } from "react";
import Web3 from "web3";
import Instagram from "../backend/build/contracts/Instagram.json";
const Home = () => {
const [account, setAccount] = useState("");
const [web3, setWeb3] = useState(null);
const [instagram, setInstagram] = useState(null);
const [postCount, setPostCount] = useState(0);
const [posts, setPosts] = useState([]);
const [buffer, setBuffer] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadBlockchainData();
}, [account]);
const ipfsClient = require("ipfs-http-client");
const projectId = "21sdaf....";
const projectSecret = "22fsa...";
const auth =
"Basic " + Buffer.from(projectId + ":" + projectSecret).toString("base64");
const client = ipfsClient({
host: "ipfs.infura.io",
port: 5001,
protocol: "https",
headers: {
authorization: auth,
},
});
const Web3Handler = async () => {
try {
const account = await window.ethereum.request({
method: "eth_requestAccounts",
});
const web3 = new Web3(window.ethereum);
setAccount(account[0]);
setWeb3(web3);
} catch (error) {
console.log(error);
}
};
const loadBlockchainData = async () => {
const web3 = new Web3(window.ethereum);
const accounts = await web3.eth.getAccounts();
setAccount(accounts[0]);
const networkId = await web3.eth.net.getId();
const networkData = Instagram.networks[networkId];
if (networkData) {
const instagram= new web3.eth.Contract(Instagram.abi, networkData.address);
setInstagram(instagram);
const postCount= await instagram.methods.postCount().call();
setPostCount(postCount);
for (let i = postCount; i >= 1; i--) {
const post = await instagram.methods.posts(i).call();
setPosts((prevPosts) => [...prevPosts, post]);
}
} else {
window.alert("Twitter contract not deployed to detected network.");
}
}
const captureFile = (event) => {
event.preventDefault();
const file = event.target.files[0];
const reader = new window.FileReader();
reader.readAsArrayBuffer(file);
reader.onloadend = () => {
setBuffer(Buffer(reader.result));
console.log("buffer", buffer);
};
};
const uploadPost = async (description) => {
setLoading(true);
try {
client.add(buffer, (error, result) => {
console.log("IPFS result", result);
if (error) {
console.error(error);
return;
}
setLoading(true);
instagram.methods
.uploadPost(result[0].hash, description)
.send({ from: account })
.on("transactionHash", (hash) => {
setLoading(false);
});
});
} catch (error) {
console.log(error);
setLoading(false);
}
};
return (
<div>
<h1>yo</h1>
</div>
);
};
export default Home;
Congrats! Your functions are all complete and now we are ready to create the front end.
In the empty div in the return above, we can add the following code:
<button onClick={Web3Handler}>Connect Wallet</button>
{account && <h1>{account}</h1>}
This will connect the user to MetaMask and also display the connected accounts address.
Now to create the form that handles uploads:
<form
onSubmit={(event) => {
event.preventDefault();
const description = event.target.imageDescription.value;
uploadPost(description);
}}
>
<input
id="file_input"
type="file"
onChange={captureFile}
/>
<input
id="imageDescription"
type="text"
placeholder="Image description..."
required
/>
<div>
<button type="submit">
upload
</button>
</div>
{loading && <h1>Loading...</h1>}
</form>
This should properly allow you to attach a file, enter a description and upload the post to the blockchain.
In order to render the posts on the page, include the following code:
{posts &&
posts.map((post, key) => {
return (
<div key={key}>
<div>
<h2>{post.description}</h2>
<img src={`https://ipfs.io/ipfs/${post.hash}`} />
</div>
</div>
);
})}
YOUR FINAL FINAL index.js FILE SHOULD LOOK LIKE THIS:
import Head from "next/head";
import { useEffect, useState } from "react";
import Web3 from "web3";
import Instagram from "../backend/build/contracts/Instagram.json";
const Home = () => {
const [account, setAccount] = useState("");
const [web3, setWeb3] = useState(null);
const [instagram, setInstagram] = useState(null);
const [postCount, setPostCount] = useState(0);
const [posts, setPosts] = useState([]);
const [buffer, setBuffer] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadBlockchainData();
}, [account]);
const ipfsClient = require("ipfs-http-client");
const projectId = "2FdliMGfWHQCzVYTtFlGQsknZvb";
const projectSecret = "2274a79139ff6fdb2f016d12f713dca1";
const auth =
"Basic " + Buffer.from(projectId + ":" + projectSecret).toString("base64");
const client = ipfsClient({
host: "ipfs.infura.io",
port: 5001,
protocol: "https",
headers: {
authorization: auth,
},
});
const Web3Handler = async () => {
try {
const account = await window.ethereum.request({
method: "eth_requestAccounts",
});
const web3 = new Web3(window.ethereum);
setAccount(account[0]);
setWeb3(web3);
} catch (error) {
console.log(error);
}
};
const loadBlockchainData = async () => {
const web3 = new Web3(window.ethereum);
const accounts = await web3.eth.getAccounts();
setAccount(accounts[0]);
const networkId = await web3.eth.net.getId();
const networkData = Instagram.networks[networkId];
if (networkData) {
const instagram = new web3.eth.Contract(
Instagram.abi,
networkData.address
);
setInstagram(instagram);
const postCount = await instagram.methods.postCount().call();
setPostCount(postCount);
for (let i = postCount; i >= 1; i--) {
const post = await instagram.methods.posts(i).call();
setPosts((prevPosts) => [...prevPosts, post]);
}
} else {
window.alert("Twitter contract not deployed to detected network.");
}
};
const captureFile = (event) => {
event.preventDefault();
const file = event.target.files[0];
const reader = new window.FileReader();
reader.readAsArrayBuffer(file);
reader.onloadend = () => {
setBuffer(Buffer(reader.result));
console.log("buffer", buffer);
};
};
const uploadPost = async (description) => {
setLoading(true);
try {
client.add(buffer, (error, result) => {
console.log("IPFS result", result);
if (error) {
console.error(error);
return;
}
setLoading(true);
instagram.methods
.uploadPost(result[0].hash, description)
.send({ from: account })
.on("transactionHash", (hash) => {
setLoading(false);
setTimeout(() => {
window.location.reload();
}, 2000);
});
});
// wait 2 seconds than reload page
} catch (error) {
console.log(error);
setLoading(false);
}
};
return (
<div>
<button onClick={Web3Handler}>Connect Wallet</button>
{account && <h1>{account}</h1>}
<form
onSubmit={(event) => {
event.preventDefault();
const description = event.target.imageDescription.value;
uploadPost(description);
}}
>
<input id="file_input" type="file" onChange={captureFile} />
<input
id="imageDescription"
type="text"
placeholder="Image description..."
required
/>
<div>
<button className="border border-black" type="submit">
upload
</button>
</div>
{loading && <h1>Loading...</h1>}
</form>
{posts &&
posts.map((post, key) => {
return (
<div key={key}>
<h2>{post.description}</h2>
<img src={`https://ipfs.io/ipfs/${post.hash}`} />
</div>
);
})}
</div>
);
};
export default Home;
If you made it this far, thank you so much for reading along and following. I hope you learned something new! :)
Top comments (0)