DEV Community

Cover image for How to Create Decentralized IPFS Hash Storage on Morph Blockchain
Azeez Abidoye
Azeez Abidoye

Posted on

How to Create Decentralized IPFS Hash Storage on Morph Blockchain

What is IPFS? 🤔

According to Wikipedia, the InterPlanetary File System (IPFS) is a decentralized protocol, hypermedia, and peer-to-peer network for distributed file storage and sharing. By using content-addressing, IPFS uniquely identifies files in a global namespace that interlinks IPFS hosts, creating a hypermedia system that enables efficient and reliable data distribution.
Interoperability with decentralized applications is an important feature of IPFS, providing a strong foundation for Web3 and blockchain ecosystems.
In this article, we will develop a decentralized application on the Morph Holesky testnet that uses Pinata as the storage system to generate and store IPFS Hash onchain.

Prerequisites 📚

  • NodeJs (Version 18 or later)
  • NPM (Version 7 or later)
  • Metamask wallet
  • Testnet ethers
  • Pinata API Key

Dev Tool 🛠️

  • Yarn
npm install -g yarn
Enter fullscreen mode Exit fullscreen mode

Step 1: Create a new React application

Let's begin by creating a new React app named ipfs-hash-storage

npm create vite@latest ipfs-hash-storage --template react
Enter fullscreen mode Exit fullscreen mode

✍️ Follow through with the prompt by selecting React and JavaScript

  • Navigate into the new React app
cd ipfs-hash-storage
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Hardhat package as a dependency

yarn add hardhat
Enter fullscreen mode Exit fullscreen mode

Step 3: Initialize Hardhat as the Ethereum Development Environment

npx hardhat init
Enter fullscreen mode Exit fullscreen mode

✍️ Follow through with the prompt by selecting Create a JavaScript project

  • Delete the files in the contracts and test directories to have a clean slate

Bonus: 💚 How to create Pinata API Key

  • Sign up/Login on pinata.cloud
  • Select API Keys in the side bar
  • Select New Key at the top right corner
  • Check Admin to access all endpoints and account settings
  • Give your key a name and select Generate Key

Generate Pinata key

✍️ This will open a modal containing all the API Key information

  • Copy the JWT token and store safely

Pinata JWT Token

  • Select Gateways in the sidebar
  • Copy the Domain link and store safely

Pinata Gateway domain

✍️ This will be used to interact with IPFS node by Pinata

Step 4: Setup the environment information

  • Install the plugin to configure the environment variables
yarn add --dev dotenv
Enter fullscreen mode Exit fullscreen mode
  • Create a new file named .env in the root directory and populate it with the following variables:
RPC_URL="https://rpc-quicknode-holesky.morphl2.io"
DEV_PRIVATE_KEY="insert-your-private-key-here"
VITE_PINATA_JWT="insert-your-jwt-token-here"
VITE_PINATA_GATEWAY="insert-your-gateway-domain-link-here"
Enter fullscreen mode Exit fullscreen mode

Step 5: Configure Hardhat for DApp Development

  • Open the hardhat.config.cjs file and configure it as follows:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

const { RPC_URL, DEV_PRIVATE_KEY } = process.env;

module.exports = {
  solidity: "0.8.25",
  paths: {
    artifacts: "./src/artifacts",
  },
  networks: {
    morphTestnet: {
      chainId: 2810,
      url: RPC_URL,
      accounts: [DEV_PRIVATE_KEY],
      gasPrice: 2000000000,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Step 6: Configure Pinata for storage purpose

  • Install Pinata plugin for configuration
yarn add pinata
Enter fullscreen mode Exit fullscreen mode
  • Navigate to the src directory and create a new file named config.js
  • Populate src/config.js file with the following code for Pinata configuration:
import { PinataSDK } from "pinata";

const pinataJwt = import.meta.env.VITE_PINATA_JWT;
const pinataGateway = import.meta.env.VITE_PINATA_GATEWAY;

export const pinata = new PinataSDK({
  pinataJwt,
  pinataGateway,
});
Enter fullscreen mode Exit fullscreen mode

Step 7: Write the Solidity code for smart contract

  • Open the contracts directory and create a new file named IpfsHashStorage.sol
  • Here is the smart contract code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

contract IpfsHashStorage {
    string private ipfsHash;

    // Hash Setter Function
    function setIPFSHash (string memory _ipfsHash) public view {
        _ipfsHash = ipfsHash;
    }

    // Hash Getter Function
    function getIPFSHash() public view returns(string memory) {
        return ipfsHash;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Compile smart contract

  • Execute the following command to compile the smart contract code
yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode
  • The compilation result should look like this
Compiled 1 Solidity file successfully (evm target: paris).
✨ Done in 3.70s.
Enter fullscreen mode Exit fullscreen mode

✍️ The Application Binary Interface (ABI) is saved in the artifacts directory, which is automatically generated in the src directory, according to the initial Hardhat configurations

Step 9: Deploy DApp on Morph testnet

  • Install Hardhat deployment package
yarn add --dev hardhat-deploy
Enter fullscreen mode Exit fullscreen mode
  • Import hardhat-deploy package into the Hardhat config file
require("hardhat-deploy");
Enter fullscreen mode Exit fullscreen mode
  • Install another Hardhat plugin to override @nomiclabs/hardhat-ethers
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
Enter fullscreen mode Exit fullscreen mode
  • Open the Hardhat config file and set up a deployer account
networks: {
     // Network config here...
},
namedAccounts: {
     deployer: {
        default: 0,
     }
}
Enter fullscreen mode Exit fullscreen mode
  • Create a new directory named deploy in the root directory
mkdir deploy
Enter fullscreen mode Exit fullscreen mode
  • Create a new file named deploy.cjs in the deploy folder
  • Populate the deploy.cjs file with the following code
module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  const args = [];
  await deploy("IpfsHashStorage", {
    contract: "IpfsHashStorage",
    args: args,
    from: deployer,
    log: true,
  });
};
module.exports.tags = ["IpfsHashStorage"];
Enter fullscreen mode Exit fullscreen mode
  • To deploy the contract on Morph Holesky testnet, execute the following command
yarn hardhat deploy --network morphTestnet 
Enter fullscreen mode Exit fullscreen mode
  • The result of the deployment should look similar to this:
Nothing to compile
deploying "IpfsHashStorage" (tx: 0x8ac1f3d2fa46bf2f925505cfd3aeadaaf4622692c6dbaf9d7ec3d2d197098a5a)...: deployed at 0x99aFF0cbBb2561d1898b6B14C46b2cb994F9eDEC with 298063 gas
✨ Done in 7.79s.
Enter fullscreen mode Exit fullscreen mode

Frontend Integration

  • Open src/App.jsx file and populate it with the following code:
  • The imports, ABI and the contract address
import React, { useState } from "react";
import { ethers } from "ethers";
import { pinata } from "./config";
import "./App.css";

// Contract ABI
import { abi } from "./artifacts/contracts/IpfsHashStorage.sol/IpfsHashStorage.json";

// Contract Address
const contractAddress = "Paste-your-contract-address-here";
Enter fullscreen mode Exit fullscreen mode
  • The state variables
function App() {
    const [selectedFile, setSelectedFile] = useState(null);
    const [ipfsHash, setIpfsHash] = useState("");
    const [storedHash, setStoredHash] = useState("");
  return (
    // Code here
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • The handler function
function App() {
    // State variables...

    // Handler Function
    const changeHandler = (event) => {
    setSelectedFile(event.target.files[0]);
    };
  return (
    // Code here
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • Add the function for storing IPFS Hash on-chain
function App() {
    // State variables...

    // Handler Function...

    // Function to Store IPFS on the Blockchain
    const storeHashOnBlockchain = async (hash) => {
        // Check if Metamask is installed
        if (typeof window.ethereum !== "undefined") {
            // Connect to Ethereum provider (Metamask)
            const provider = new ethers.providers.Web3Provider(window.ethereum);
            const signer = provider.getSigner();

            // Create a contract instance
            const contract = new ethers.Contract(contractAddress, abi, signer);
            try {
            // Send transaction to store IPFS Hash on the blockchain
            const txResponse = await contract.setIPFSHash(hash);
            await txResponse.wait();
            } catch (error) {
            console.log("Failed to store IPFS Hash on blockchain", error);
            }
        }
    };

  return (
    // Code here
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • Add the function for retrieving the IPFS Hash
function App() {
    // State variables...

    // Handler Function...

    // Function to Store IPFS on the Blockchain...

    // Function to Retrieve Hash from the Blockchain
    const retrieveHashFromBlockchain = async () => {
        // Check if Metamask is installed
        if (typeof window.ethereum !== "undefined") {
            // Connect to Ethereum provider (Metamask)
            const provider = new ethers.providers.Web3Provider(window.ethereum);
            const signer = provider.getSigner();

            // Create a contract instance
            const contract = new ethers.Contract(contractAddress, abi, signer);
            try {
            // Send transaction to retrieve IPFS Hash from the blockchain
            const storedHash = await contract.getIPFSHash();
            setStoredHash(storedHash);
            } catch (error) {
            console.log("Failed to retrieve IPFS Hash from blockchain", error);
            }
        }
    };

  return (
    // Code here
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • Lastly, add the function to upload files to Pinata cloud
function App() {
    // State variables...

    // Handler Function...

    // Function to Store IPFS on the Blockchain...

    // Function to Retrieve Hash from the Blockchain...

    // Upload Function
    const handleUpload = async () => {
        const response = await pinata.upload.file(selectedFile);
        const ipfsHash = response.cid;
        setIpfsHash(ipfsHash);
        await storeHashOnBlockchain(ipfsHash);
        setIpfsHash(" ");
    };

  return (
    // Code here
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • Update the JSX component
function App() {

  return (
        <>
        <div className="app-container">
        <h1>Store IPFS On-chain</h1>
        <div className="upload-section">
            <label className="form-label">Choose File</label>
            <input
            type="file"
            onChange={changeHandler}
            className="file-input"
            ></input>
            <button className="upload-button" onClick={handleUpload}>
            Upload
            </button>
        </div>
        {ipfsHash && (
            <div className="result-section">
            <p>IPFS Hash: {ipfsHash}</p>
            </div>
        )}
        <div className="retrieve-section">
            <button
            className="retrieve-button"
            onClick={retrieveHashFromBlockchain}
            >
            Retrieve Hash
            </button>
        </div>
        {storedHash && (
            <div>
            <p>Retrieved Hash: {storedHash}</p>
            </div>
        )}
        </div>
    </>
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • Update the App.css file with the following code:
/* App.css */
.app-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #f4f4f9;
  font-family: Arial, sans-serif;
}

.upload-section {
  background-color: #fff;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
  text-align: center;
}

.form-label {
  display: block;
  margin-bottom: 10px;
  font-size: 18px;
  font-weight: bold;
  color: #333;
}

.file-input {
  margin-bottom: 15px;
}

.upload-button {
  background-color: #4caf50;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.3s ease;
}

.upload-button:hover {
  background-color: #45a049;
}

.result-section {
  margin-top: 20px;
  padding: 10px 20px;
  background-color: #e0f7fa;
  border-radius: 5px;
  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
}

.result-section p {
  margin: 0;
  font-size: 16px;
  color: #333;
}
Enter fullscreen mode Exit fullscreen mode
  • Execute the following command to run the React App
yarn run dev
Enter fullscreen mode Exit fullscreen mode

DApp UI

  • To view the uploaded file in your storage, select Files in the sidebar

✍️ This will provide a list of all uploaded files in your storage.

Pinata Files Storage

  • Choose a file to preview in a new tab

a-dance-morph-preview

Conclusion

Creating and retrieving an IPFS hash on the blockchain with the Pinata platform as storage is covered in this tutorial. Nevertheless, the InterPlanetary File System and its potential for decentralized systems are not limited to that. Hopefully, I will create a more advanced decentralized app with a similar storage in the near future.

Top comments (0)