Intermediate
How to use IPFS on harmony blockchain
In this tutorial, we will be learning how to use the API to swap assets between different blockchain networks. We will be covering the basics of setting up the environment, making API calls and understanding the responses. By the end of this tutorial, you will have a solid understanding of how the API works and be able to create your own use cases for swapping assets. So, get ready to dive into the world of blockchain interoperability!
Context
The Harmony blockchain can be used for data storage, but it is important to consider the cost. One byte of data can cost 42,107 gas or 0.00042017 ONE tokens, which is approximately $0.00003092031 at the current market value. While this may not seem significant for just one byte of data, the cost can quickly add up if you are storing larger files. For example, a file with 1 GB of data (1,000,000,000 bytes) would cost $30920.31. A solution to this problem is using IPFS (InterPlanetary File System) which is a cheaper option to store data on the blockchain.
What is IPFS?
IPFS (InterPlanetary File System) is a decentralized system for storing and accessing files, websites, applications, and data. By using IPFS as a storage solution, you don’t need to store entire files on the Harmony blockchain, you can just store the hash of the IPFS file on the blockchain, making it a much more cost-effective option.
In this tutorial, we will demonstrate how to use IPFS to store files off-chain and store the file’s hash on the blockchain. We will also retrieve the data from the blockchain and display it on a webpage.
Github Link
https://github.com/harmony-one/harmony-ipfs
Prerequisites
In this entire tutorial i’m using windows 10, but you can use any other os.
Step 1 – Install IPFS
To install IPFS on your machine, there are so many ways to do it. You can see for yourself here Kubo | IPFS Docs . After that go to your command prompt and type:
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "["""webui://-""", """http://localhost:3000""", """http://127.0.0.1:5001""", """https://webui.ipfs.io"""]" ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "["""PUT""", """POST"""]"
This command needed so that we can upload our file without any CORS problem. After that run this command to run ipfs locally:
ipfs daemon
Step 2 – Install Metamask
There is also so many way to download metamask. If you’re using chrome you can download it from here https://metamask.io/download.html, and click on install metamask for chrome
after you install the metamask you need to setup the metamask so that it can interact with harmony blockchain click on Custom RPC
like below :
Then after that write this one by one:
Network Name : Harmony Testnet New RPC URL : https://api.s0.b.hmny.io Chain ID : 1666700000 Currency Symbol : ONE Block Explorer URL : https://explorer.pops.one/
Now you have an harmony one account on metamask testnet we need to fill some harmony one token you can do that by going to harmony one testnet faucet like this one Faucet || Harmony Testnet , to get your address click on the three dot and click on view in explorer like this :
you will then get your address like this :
Put it in the harmony testnet faucet and click send me and you will get your harmony one token filling up.
Step 3 – Create a smart contract
First we need to install truffle. To install truffle go to your command prompt and type:
npm install -g truffle
Now create a folder and go inside that folder and type on the command prompt:
truffle init
After you run that command basically you will get some file and folder like this:
contracts/ migrations/ test/ truffle-config.js
So now i want you to go to contract
folder and create a file called IPFSstorage.sol
and paste this code in that file:
pragma solidity 0.8.6; contract IPFSstorage { string ipfsHash; function sendHash(string memory x) public { ipfsHash = x; } function getHash() public view returns (string memory x) { return ipfsHash; } }
So this is actually a really basic smart contract with the purpose of storing the hash of the file to the blockchain. sendHash
is to store the hash, and getHash
is to get the hash. After that go to the migrations
folder and create a file called 2_ipfs_storage_migration.js
and paste this code into that file :
var IPFSstorage = artifacts.require("./IPFSstorage.sol"); module.exports = function(deployer) { // Demo is the contract's name deployer.deploy(IPFSstorage); };
This code is basically used for deploying our contract to our testnet later using truffle. Now go back to the root of our folder and look for truffle-config.js
file and replace the code inside of that file with this :
require('dotenv').config() const PrivateKeyProvider = require('./private-provider') const networkId = { Mainnet: 1666600000, Testnet: 1666700000 } module.exports = { networks: { localnet: { provider: () => { return new PrivateKeyProvider(process.env.LOCALNET_PRIVATE_KEY, 'http://localhost:9500', networkId.Testnet) }, network_id: networkId.Testnet }, testnet: { provider: () => { return new PrivateKeyProvider(process.env.TESTNET_PRIVATE_KEY, 'https://api.s0.b.hmny.io', networkId.Testnet) }, network_id: networkId.Testnet }, mainnet: { provider: () => { return new PrivateKeyProvider(process.env.MAINNET_PRIVATE_KEY, 'https://api.s0.t.hmny.io', networkId.Mainnet) }, network_id: networkId.Mainnet } }, compilers: { solc: { version: '0.8.6' }, } }
After that create a file called private-provider.js
and copy paste this code :
const ProviderEngine = require('@trufflesuite/web3-provider-engine'); const WalletSubprovider = require('@trufflesuite/web3-provider-engine/subproviders/wallet'); const RpcSubprovider = require('@trufflesuite/web3-provider-engine/subproviders/rpc'); const EthereumjsWallet = require('ethereumjs-wallet'); // ChainIdSubProvider function ChainIdSubProvider(chainId) { this.chainId = chainId; } ChainIdSubProvider.prototype.setEngine = function (engine) { const self = this; if (self.engine) return; self.engine = engine; }; ChainIdSubProvider.prototype.handleRequest = function (payload, next, end) { if ( payload.method == 'eth_sendTransaction' && payload.params.length > 0 && typeof payload.params[0].chainId == 'undefined' ) { payload.params[0].chainId = this.chainId; } next(); }; // NonceSubProvider function NonceSubProvider() { } NonceSubProvider.prototype.setEngine = function (engine) { const self = this; if (self.engine) return; self.engine = engine; }; NonceSubProvider.prototype.handleRequest = function (payload, next, end) { if (payload.method == 'eth_sendTransaction') { this.engine.sendAsync( { jsonrpc: '2.0', id: Math.ceil(Math.random() * 4415011859092441), method: 'eth_getTransactionCount', params: [payload.params[0].from, 'latest'], }, (err, result) => { const nonce = typeof result.result == 'string' ? result.result == '0x' ? 0 : parseInt(result.result.substring(2), 16) : 0; payload.params[0].nonce = nonce || 0; next(); }); } else { next(); } }; // PrivateKeyProvider function PrivateKeyProvider(privateKey, providerUrl, chainId) { if (!privateKey) { throw new Error( `Private Key missing, non-empty string expected, got "${privateKey}"` ); } if (!providerUrl) { throw new Error( `Provider URL missing, non-empty string expected, got "${providerUrl}"` ); } this.wallet = EthereumjsWallet.default.fromPrivateKey(Buffer.from(privateKey, 'hex')); this.address = `0x${this.wallet.getAddress().toString('hex')}`; this.engine = new ProviderEngine({ useSkipCache: false }); this.engine.addProvider(new ChainIdSubProvider(chainId)); this.engine.addProvider(new NonceSubProvider()); this.engine.addProvider(new WalletSubprovider(this.wallet, {})); this.engine.addProvider(new RpcSubprovider({ rpcUrl: providerUrl })); this.engine.start(); } PrivateKeyProvider.prototype.sendAsync = function (payload, callback) { return this.engine.sendAsync.apply(this.engine, arguments); }; PrivateKeyProvider.prototype.send = function () { return this.engine.send.apply(this.engine, arguments); }; module.exports = PrivateKeyProvider;
Now create a file called package.json
and copy paste this code :
{ "name": "harmony-box", "version": "1.0.0", "dependencies": { "@openzeppelin/contracts": "^3.4.0", "@trufflesuite/web3-provider-engine": "^15.0.13-1", "ethereumjs-wallet": "^1.0.1", "dotenv": "^8.2.0", "truffle": "^5.1.66", "solc": "^0.7.6" } }
And run npm install
to install all the dependencies. Now create a file called .env
and copy paste this code:
LOCALNET_PRIVATE_KEY='ENTER_PRIVATE_KEY_HERE' TESTNET_PRIVATE_KEY='ENTER_PRIVATE_KEY_HERE' MAINNET_PRIVATE_KEY='ENTER_PRIVATE_KEY_HERE'
Because we are gonna use testnet, we need to get our private key and set it in the .env
file, on the TESTNET_PRIVATE_KEY
variable. Let’s go to the metamask and get our private key. Click the three dot again and click account detail and click export private key :
export
After you input your password you should see your private key, after that just copy past it in the .env
file, on the TESTNET_PRIVATE_KEY
variable. After that to deploy our smart contract you just need to run this command :
truffle migrate --network testnet --reset
Wait for it to complete and then you will get a contract address like this :
contract address
Save that contract address in the safe place, because we will need it in the next step.
Step 4 – Integrating IPFS + Harmony Blockchain
First thing first, we need to create a react application to communicate with the harmony blockchain and IPFS, much more easily, we gonna use nextjs framework for this one so to create it first we need to run this command :
npx create-next-app harmony-ipfs --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"
It will create a new directory called harmony-ipfs
and install all the dependencies for you. We also need to style our app with some css, so we gonna use chakra-ui for this. Go inside the harmony-ipfs
directory and install the dependencies :
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4 ipfs-http-client
After that create a directory called abi
and copy paste the abi file called IPFSstorage.json
from build/contracts
folder on the root of the project.
Now go inside pages
folder and create a file called _app.js
and copy paste this code :
import { ChakraProvider } from "@chakra-ui/react" function MyApp({ Component, pageProps }) { return ( <ChakraProvider> <Component {...pageProps} /> </ChakraProvider> ) } export default MyApp
Now go to index.js and replace the code with this one :
import Head from 'next/head' import { Container } from "@chakra-ui/react"; export default function Home() { return ( <Container> <Head> <title>IPFS + Harmony</title> <link rel="icon" href="/favicon.ico" /> </Head> </Container> ) }
Add all of the import first below import { Container } from "@chakra-ui/react";
:
import { Button } from "@chakra-ui/react"; import { Center } from "@chakra-ui/react"; import Web3 from "web3"; import { Table, Thead, Tbody, Tr, Th, Td } from "@chakra-ui/react"; import {useRef,useState, useEffect} from "react" import { create } from 'ipfs-http-client' import IPFScontract from "../abi/IPFSstorage.json"
Let’s create all the state that we need so that our application can function properly, below export default function Home() {
copy paste this code :
const [inputFile,setInputFile]=useState(null) const [ethereumEnabled,setEthereumEnabled]=useState(false) const [web3,setWeb3]=useState(null) const [transaction,setTransaction]=useState(null) const [ipfsHash,setIpfsHash]=useState(null)
And then let’s create a function that can be used for later to connect to our metamask put it below the state :
const ethEnabled = async () => { if (window.ethereum) { await window.ethereum.send('eth_requestAccounts'); setWeb3(new Web3(window.ethereum)) setEthereumEnabled(true) return true; } setEthereumEnabled(false) return false; }
And then let’s create some code that can trigger the choose file button and also upload to ipfs :
const fileInput = useRef(null); // trigger choose file button for later const client = create('http://localhost:5001/api/v0') // function to create a client to upload to ipfs later async function onChange(e){ // function to detect file change const file = e.target.files[0] setInputFile(file) }
Let’s create a function that can be used to upload the file to ipfs and harmony blockchain :
async function uploadFileToIPFS(file){ try { const added = await client.add(file) let contract = new web3.eth.Contract(IPFScontract.abi, "<contract address>") const from=(await web3.eth.getAccounts())[0] const contractTransaction=await contract.methods.sendHash(added.path).send({ from }) setTransaction(contractTransaction) console.log(contractTransaction) setInputFile(null) } catch (error) { console.log('Error uploading file: ', error) return false } }
If you remember before you save the contract address of IPFSstorage contract right replace <contract address>
with your saved contract address, then after that let’s create a function that can be used to get the hash of the file that we uploaded to IPFS :
async function getIPFSHash(){ try { let contract = new web3.eth.Contract(IPFScontract.abi, "<contract address>") const contractIpfsHash=await contract.methods.getHash().call() setIpfsHash(contractIpfsHash) } catch (error) { console.log('Error getting hash: ', error) return false } }
Last but not least, create a html that can be used to upload and display our blockchain transaction also our ipfs hash that we get from harmony blockchain copy paste this code below </Head>
tag :
<input ref={fileInput} type="file" style={{visibility:"hidden"}} onChange={onChange} /> <Center mt="10px"><b>Upload file to ipfs and harmony blockchain</b></Center> <Center h="100px" color="white"> {!ethereumEnabled?<Button colorScheme="blue" onClick={()=>ethEnabled()}>Login with metamask</Button>:<Button colorScheme="orange">Logout</Button>} </Center> <Center style={{display:!ethereumEnabled?"none":"flex"}} h="100px" color="white"> <Button colorScheme="blue" onClick={()=>fileInput.current.click()}>{inputFile?inputFile.name:"Choose File"}</Button> </Center> <Center style={{display:!ethereumEnabled||!inputFile?"none":"flex"}} h="100px" color="white"> <Button colorScheme="green" onClick={()=>uploadFileToIPFS(inputFile)}>Upload File</Button> </Center> <Center style={{display:!ethereumEnabled||!transaction?"none":"flex"}}><b>Transaction Detail</b></Center> <Table style={{display:!ethereumEnabled||!transaction?"none":""}} variant="simple"> <Thead> <Tr> <Th>Key</Th> <Th>Value</Th> </Tr> </Thead> <Tbody> <Tr> <Td>Transaction Hash</Td> <Td>{transaction?.transactionHash}</Td> </Tr> <Tr> <Td>Status</Td> <Td>{transaction?.status}</Td> </Tr> <Tr> <Td>Gas Used</Td> <Td>{transaction?.gasUsed}</Td> </Tr> <Tr> <Td>Block Number</Td> <Td>{transaction?.blockNumber}</Td> </Tr> <Tr> <Td>Block Hash</Td> <Td>{transaction?.blockHash}</Td> </Tr> </Tbody> </Table> <Center style={{display:!ethereumEnabled||!transaction?"none":"flex"}} h="100px" color="white"> <Button colorScheme="blue" onClick={()=>getIPFSHash()}>Get IPFS URL</Button> </Center> <Center><a target="_blank" href={ipfsHash?`http://localhost:8080/ipfs/${ipfsHash}`:"#"}>{ipfsHash?`http://localhost:8080/ipfs/${ipfsHash}`:""}</a></Center>
This is our final code of index.js
file :
import Head from 'next/head' import { Container } from "@chakra-ui/react"; import { Button } from "@chakra-ui/react"; import { Center } from "@chakra-ui/react"; import Web3 from "web3"; import { Table, Thead, Tbody, Tr, Th, Td } from "@chakra-ui/react"; import {useRef,useState, useEffect} from "react" import { create } from 'ipfs-http-client' import IPFScontract from "../abi/IPFSstorage.json" export default function Home() { const [inputFile,setInputFile]=useState(null) const [ethereumEnabled,setEthereumEnabled]=useState(false) const [web3,setWeb3]=useState(null) const [transaction,setTransaction]=useState(null) const [ipfsHash,setIpfsHash]=useState(null) const ethEnabled = async () => { if (window.ethereum) { await window.ethereum.send('eth_requestAccounts'); setWeb3(new Web3(window.ethereum)) setEthereumEnabled(true) return true; } setEthereumEnabled(false) return false; } const fileInput = useRef(null); // trigger choose file button for later const client = create('http://localhost:5001/api/v0') // function to upload to ipfs async function onChange(e){ // function to detect file change const file = e.target.files[0] setInputFile(file) } async function uploadFileToIPFS(file){ try { const added = await client.add(file) let contract = new web3.eth.Contract(IPFScontract.abi, "<contract address>") const from=(await web3.eth.getAccounts())[0] const contractTransaction=await contract.methods.sendHash(added.path).send({ from }) setTransaction(contractTransaction) console.log(contractTransaction) setInputFile(null) } catch (error) { console.log('Error uploading file: ', error) return false } } async function getIPFSHash(){ try { let contract = new web3.eth.Contract(IPFScontract.abi, "<contract address>") const contractIpfsHash=await contract.methods.getHash().call() setIpfsHash(contractIpfsHash) } catch (error) { console.log('Error getting hash: ', error) return false } } return ( <Container> <Head> <title>IPFS + Harmony</title> <link rel="icon" href="/favicon.ico" /> </Head> <input ref={fileInput} type="file" style={{visibility:"hidden"}} onChange={onChange} /> <Center mt="10px"><b>Upload file to ipfs and harmony blockchain</b></Center> <Center h="100px" color="white"> {!ethereumEnabled?<Button colorScheme="blue" onClick={()=>ethEnabled()}>Login with metamask</Button>:<Button colorScheme="orange">Logout</Button>} </Center> <Center style={{display:!ethereumEnabled?"none":"flex"}} h="100px" color="white"> <Button colorScheme="blue" onClick={()=>fileInput.current.click()}>{inputFile?inputFile.name:"Choose File"}</Button> </Center> <Center style={{display:!ethereumEnabled||!inputFile?"none":"flex"}} h="100px" color="white"> <Button colorScheme="green" onClick={()=>uploadFileToIPFS(inputFile)}>Upload File</Button> </Center> <Center style={{display:!ethereumEnabled||!transaction?"none":"flex"}}><b>Transaction Detail</b></Center> <Table style={{display:!ethereumEnabled||!transaction?"none":""}} variant="simple"> <Thead> <Tr> <Th>Key</Th> <Th>Value</Th> </Tr> </Thead> <Tbody> <Tr> <Td>Transaction Hash</Td> <Td>{transaction?.transactionHash}</Td> </Tr> <Tr> <Td>Status</Td> <Td>{transaction?.status}</Td> </Tr> <Tr> <Td>Gas Used</Td> <Td>{transaction?.gasUsed}</Td> </Tr> <Tr> <Td>Block Number</Td> <Td>{transaction?.blockNumber}</Td> </Tr> <Tr> <Td>Block Hash</Td> <Td>{transaction?.blockHash}</Td> </Tr> </Tbody> </Table> <Center style={{display:!ethereumEnabled||!transaction?"none":"flex"}} h="100px" color="white"> <Button colorScheme="blue" onClick={()=>getIPFSHash()}>Get IPFS URL</Button> </Center> <Center><a target="_blank" href={ipfsHash?`http://localhost:8080/ipfs/${ipfsHash}`:"#"}>{ipfsHash?`http://localhost:8080/ipfs/${ipfsHash}`:""}</a></Center> </Container> ) }
If you’re successfully follow this tutorial you will get something like this :
Click on confirm and wait for a while.
After that if you get your transaction detail you’re successfully uploaded your file to IPFS and harmony blockchain.
If you click on “Get IPFS URL” you will get your IPFS URL.
Conclusion
Congratulations! You have successfully uploaded your file to IPFS and the Harmony blockchain. This is a basic example of how to upload and retrieve a file from the Harmony blockchain. You can use your imagination and come up with more complex use cases, such as storing NFT metadata.
Please note that in this example, we are using a local IPFS node. If the node goes down, you will not be able to retrieve your file. An alternative is to use a service like Infura, which is a free IPFS service with some limitations. You can also automatically pin your file to Infura by changing “localhost:8080” to “ipfs.infura.io.” However, be cautious when uploading private files as they cannot be unpinned.
That’s it for now. If you have any questions or suggestions, please feel free to ask in the Official Harmony Discord.