DEV Community

Chris Abdo
Chris Abdo

Posted on

How to create an Instagram clone on the Polygon Network using Next.js and Truffle

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.

  1. change into the project directory
    cd instagram-polygon

  2. install the required packages
    yarn add web3 truffle ipfs-http-client@33.0.1

  3. create a folder named backend

  4. 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",
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. A public integer postCount that tracks the total amount of posts
  2. A mapping of the posts, which allows us to sort by id
  3. 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)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    }
  };
Enter fullscreen mode Exit fullscreen mode

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.");
    }
  };
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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,
    },
  });

Enter fullscreen mode Exit fullscreen mode

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);
    };
  };
Enter fullscreen mode Exit fullscreen mode

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);
    }
  };
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
          );
        })}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

If you made it this far, thank you so much for reading along and following. I hope you learned something new! :)

Top comments (0)