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
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
✍️ Follow through with the prompt by selecting
React
andJavaScript
- Navigate into the new React app
cd ipfs-hash-storage
Step 2: Install Hardhat package as a dependency
yarn add hardhat
Step 3: Initialize Hardhat as the Ethereum Development Environment
npx hardhat init
✍️ Follow through with the prompt by selecting
Create a JavaScript project
- Delete the files in the
contracts
andtest
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
✍️ This will open a modal containing all the API Key information
- Copy the
JWT
token and store safely
- Select
Gateways
in the sidebar - Copy the
Domain
link and store safely
✍️ 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
- 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"
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,
},
},
};
Step 6: Configure Pinata for storage purpose
- Install Pinata plugin for configuration
yarn add pinata
- Navigate to the
src
directory and create a new file namedconfig.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,
});
Step 7: Write the Solidity code for smart contract
- Open the
contracts
directory and create a new file namedIpfsHashStorage.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;
}
}
Step 8: Compile smart contract
- Execute the following command to compile the smart contract code
yarn hardhat compile
- The compilation result should look like this
Compiled 1 Solidity file successfully (evm target: paris).
✨ Done in 3.70s.
✍️ The Application Binary Interface (ABI) is saved in the
artifacts
directory, which is automatically generated in thesrc
directory, according to the initial Hardhat configurations
Step 9: Deploy DApp on Morph testnet
- Install Hardhat deployment package
yarn add --dev hardhat-deploy
- Import
hardhat-deploy
package into the Hardhat config file
require("hardhat-deploy");
- Install another Hardhat plugin to override
@nomiclabs/hardhat-ethers
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
- Open the Hardhat config file and set up a deployer account
networks: {
// Network config here...
},
namedAccounts: {
deployer: {
default: 0,
}
}
- Create a new directory named
deploy
in the root directory
mkdir deploy
- Create a new file named
deploy.cjs
in thedeploy
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"];
- To deploy the contract on Morph Holesky testnet, execute the following command
yarn hardhat deploy --network morphTestnet
- 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.
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";
- The state variables
function App() {
const [selectedFile, setSelectedFile] = useState(null);
const [ipfsHash, setIpfsHash] = useState("");
const [storedHash, setStoredHash] = useState("");
return (
// Code here
)
}
export default App;
- The handler function
function App() {
// State variables...
// Handler Function
const changeHandler = (event) => {
setSelectedFile(event.target.files[0]);
};
return (
// Code here
)
}
export default App;
- 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;
- 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;
- 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;
- 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;
- 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;
}
- Execute the following command to run the React App
yarn run dev
- 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.
- Choose a file to preview in a new tab
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)