Recently, a tool-type Dapp has been developed, mainly allowing users to upload OPML files of RSS Feeds, upload them to IPFS, generate a CID, and then use a smart contract to map the wallet address to the CID, supporting simple file operations on the web. The goal is to enable each user to maintain their own OPML file in a decentralized manner through this Dapp. However, this simple goal encountered many issues during development, so I decided to write a blog to document it for reference.
(I am still a student and have only recently started exploring Dapp development out of interest, with limited technical skills.)
Technology Stack Used#
- React + Vite
Building the front-end static pages. - RainbowKit
A universal wallet connection component that easily customizes wallet connections, blockchain switching, and wallet information viewing. - Wagmi
A React Hooks library for interacting with Ethereum wallets and blockchains. It provides a simple interface for connecting wallets, fetching on-chain data, calling contracts, and more. - Web3.js
Used for calling smart contracts and interacting with the blockchain. - Kubo-rpc-client
A client library for communicating with IPFS nodes. Uploading files, fetching files, and pinning files through IPFS's RPC interface. - Remix IDE
Used for writing and deploying smart contracts, suitable for simple smart contract development. - fast-xml-parser
Used for parsing and constructing OPML files, enabling basic add, delete, and update operations on OPML.
Dapp Implementation Goals#
When users start using the Dapp, three initialization steps are required: the first step is to connect the wallet, the second step is to connect to the Kubo gateway, and the third step is to import the OPML file, supporting import from local or via CID from IPFS, which can be automatically obtained through blockchain Map information.
After completing the initialization steps, users can perform some basic operations on the OPML file. After completing the operations, users will re-upload the file to IPFS, obtain a new CID, and finally call the smart contract to write the new CID to the blockchain.
Smart Contract Part#
The logic of the smart contract for this project is very simple; it just maintains a mapping table of wallet addresses to CIDs, providing query and update operations. For simple smart contracts, you can directly write and deploy them using Remix IDE. This avoids most of the installation environment hassles, making it very convenient to deploy contracts to various blockchains using Remix IDE.
Below is the Solidity contract I wrote.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract RSSFeedStorage {
address private immutable owner;
mapping(address => string) private ipfsHashes;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can set the IPFS hash");
_;
}
event IPFSHashUpdated(string newHash);
function updateIPFSHash(string memory _ipfsHash) public onlyOwner {
if (
keccak256(bytes(ipfsHashes[owner])) != keccak256(bytes(_ipfsHash))
) {
ipfsHashes[owner] = _ipfsHash;
emit IPFSHashUpdated(_ipfsHash);
}
}
function getIPFSHash(address user) public view returns (string memory) {
return ipfsHashes[user];
}
}
After completing the smart contract writing, compile it to obtain a universal ABI file. Using the web3.js library, based on this ABI file combined with the actual deployed address on different chains, you can achieve the same interaction logic across various chains. By deploying the contract to different blockchains, you can obtain different contract addresses, maintaining a contract address mapping table in the React project, allowing the Dapp to call different smart contracts on different chains.
For this project, the confirmation speed of the blockchain is not very critical; the number of operations may be relatively high, so some Layer 2 blockchains can be chosen to significantly reduce the costs of contract interactions, or use free testnets.
Frontend Contract Interaction Part#
The frontend contract interaction part mainly uses the RainbowKit and Wagmi libraries. RainbowKit provides a customizable wallet connection component, essentially providing all the interaction logic for connecting wallets, viewing wallet information, and switching the current chain of the wallet. Developers only need to modify the styles on the default components.
When using RainbowKit's components, you need to apply for a projectId from WalletConnect Cloud and configure it in RainbowkitConfig. Using the Provider including the current component allows you to use the RainbowKit hook in the project.
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { optimism, sepolia } from "wagmi/chains";
const config = getDefaultConfig({
appName: "AppName",
projectId: 'projectId',
chains: [sepolia, optimism],
ssr: false,
});
const queryClient = new QueryClient();
const App = () => {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
{/* Your App */}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
};
Calling the smart contract can be done using the web3.js library. By providing the ABI file and contract address, you can call the smart contract. To call the getIPFSHash method of the above smart contract, you need to pass in a wallet address. At this point, you can use Wagmi Hooks to get the wallet address. Wagmi provides many blockchain-related hooks to check the user address, view the current chain ID, and other operations.
Below is a simple example of calling the smart contract using web3.js.
//Example React Provider component.ts
import contractABI from "./RSSFeedStorage.json";
import Web3 from "web3";
// Define contract addresses for different chains
// Check specific blockchain IDs at https://wagmi.sh/react/api/chains#supported-chains
const contractAddresses: { [key: number]: { address: string } } = {
11155111: { // Sepolia
address: "0x63Bbcd45b669367034680093CeF5B8BFEee62C4d"
},
10: { // Optimism
address: "0x2F4508a5b56FF1d0CbB2701296C6c3EF8eE7D6B5"
},
};
// Call the contract's updateIPFSHash method
const updateIPFSAddress = async (
ipfsPath: string,
address: string,
chainId: number
) => {
const contractAddress = contractAddresses[chainId]?.address;
if (!contractAddress) {
throw new Error("contract address is not found");
}
const contract = new web3.eth.Contract(contractABI, contractAddress);
const ipfsAddress = await contract.methods
.updateIPFSHash(ipfsPath)
.send({ from: address });
return ipfsAddress;
};
// Call the contract's getIPFSHash method
const getIPFSAddress = async (address: string, chainId: number) => {
const contractAddress = contractAddresses[chainId]?.address;
if (!contractAddress) {
throw new Error("contract address is not found");
}
const contract = new web3.eth.Contract(contractABI, contractAddress);
const ipfsAddress = await contract.methods.getIPFSHash(address).call();
return ipfsAddress;
};
In the React component, use the Wagmi Hook to call the above methods.
import { useAccount, useChainId } from "wagmi";
import { useDapp } from "../providers/DappProvider";
const Example = () => {
const chainId = useChainId();
const { address, isConnected } = useAccount();
const { getIPFSAddress, updateIPFSAddress } = useDapp();
const ipfsPath = "bafkreie5duvimf3er4ta5brmvhm4axzj3tnclin3kbujmhzv3haih52adm"
return (
<>
<button onClick={() => {
updateIPFSAddress(ipfsPath, address, chainId);
}}>Update IPFS Address<button/>
<button onClick={() => {
getIPFSAddress(address, chainId);
}}>Get IPFS Address<button/>
</>
)
}
export default Example
IPFS Storage Part#
The IPFS storage part mainly uses the Kubo-rpc-client library. Kubo is the implementation of IPFS in Golang, and common IPFS Desktop and the IPFS service built into the Brave browser are both based on Kubo. After connecting to the Kubo gateway using the Kubo-rpc-client library, you can easily upload, download, and pin files.
Note: In the Kubo configuration file, cross-origin settings must be made; otherwise, you cannot connect to the Kubo gateway.
{
"API": {
"HTTPHeaders": {
"Access-Control-Allow-Credentials": [
"true"
],
"Access-Control-Allow-Headers": [
"Authorization"
],
"Access-Control-Allow-Methods": [
"PUT",
"GET",
"POST",
"OPTIONS"
],
"Access-Control-Allow-Origin": [
"*"
],
"Access-Control-Expose-Headers": [
"Location"
]
}
},
...
}
Below is a simple example of creating a Kubo-rpc-client instance and performing uploads and downloads.
import { create, KuboRPCClient } from "kubo-rpc-client";
// Connect to the Kubo gateway and create a kubo-rpc-client instance
const connectKubo = async (KuboUrl: string) => {
try {
const KuboClient = create({ KuboUrl });
} catch (error) {
console.error("Can't connect to Kubo gateway:", error);
throw error;
}
};
// Upload a file to IPFS
const uploadOpmlToIpfs = async (opml) => {
try {
const buildedOpml = buildOpml(opml);
// The add method's pin parameter defaults to true, meaning files are pinned to IPFS after upload. For more parameters, see
// https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md#ipfsadddata-options
const res = await kuboClient?.add(buildedOpml, { cidVersion: 1 });
} catch (error) {
console.error("Can't upload OPML to IPFS:", error);
}
};
// Fetch a file from IPFS
// Since Cat in IPFS is an asynchronous iterator, write a utility method to get the complete data.
export async function catFileFromPath(path: string, kubo: KuboRPCClient) {
const chunks = [];
for await (const chunk of kubo.cat(path)) {
chunks.push(chunk);
}
const fullData = new Uint8Array(chunks.reduce((acc, curr) => acc + curr.length, 0));
let offset = 0;
for (const chunk of chunks) {
fullData.set(chunk, offset);
offset += chunk.length;
}
return fullData;
}
// Call the method to decode binary data into text.
const handleImportFromIpfs = async (ipfsPath) => {
try {
const res = await catFileFromPath(importIpfsPath, kuboClient);
const opmlText = new TextDecoder().decode(res);
} catch (error) {
console.error("Can't import OPML from IPFS:", error);
}
};
Frontend Static File Deployment#
After completing the front-end page writing with React + Vite, use Vite to compile the static files, upload them to the compiled dist folder using IPFS Desktop, and pin them locally. Thus, the Dapp has been deployed to a decentralized network, but its usability is still very limited. The compiled static files are only usable locally, and their usability in other environments is entirely a matter of luck. Ultimately, I found a compromise between centralization and decentralization by using Fleek services.
Fleek is an automated deployment and static website hosting platform similar to Vercel, but it publishes static websites to IPFS and comes with a CDN. For browsers in the IPFS environment, it provides IPFS services, and for browsers without IPFS environments, it also provides services, greatly increasing the usability of the Dapp. Most importantly, it offers web2 domain resolution functionality, allowing you to avoid maintaining IPNS for the front-end static files locally and using long CID addresses for access.
The usage is similar to Vercel; connect to the GitHub repository, configure the deployment settings, and deploy with one click. Then bind your own domain in Custom Domains. Fleek also supports ENS and HNS.
IPFS File Reliability Guarantee#
The OPML files uploaded using Kubo-rpc-client will be pinned on Kubo. The speed and reliability of fetching files from the same Kubo service are acceptable. However, the reliability in different network environments is still questionable. Many times, I have already obtained the CID through blockchain mapping, but fetching the CAT file takes an extremely long time, or even fails. At this point, IPFS service providers are still needed. There are many service providers offering IPFS storage, such as:
- Panata
- Filebase
- Infura
- Fleek
These service providers all have certain free storage quotas, ranging from a few gigabytes to over ten gigabytes, which is sufficient to handle storing OPML, a plain text file, when the user base is small.
However, the SDKs of Panata and Filebase only support uploading files in backend environments, making it impossible for pure static pages to use their services. Another major pitfall is that these two service providers do not allow free accounts to store HTML files, but since I am storing OPML, they still recognize it as HTML and do not allow me to store it. I am also unclear about how to activate IPFS storage permissions for free accounts on Infura (my research is not deep, so if there are implementation solutions, please point them out).
In the end, I chose Fleek's IPFS storage service, but it seems that Fleek has two versions of the website, one is fleek.co and the other is fleek.xyz. My front-end page is deployed on fleek.co, but the IPFS storage service is only available on fleek.xyz.
Using Fleek's IPFS storage service requires applying for a ClientID, which needs to be requested in Fleek's CLI, so you need to install Fleek's SDK.
# You need to have Nodejs >= 18.18.2
npm install -g @fleek-platform/cli
# Log in to your Fleek account
fleek login
# Create a ClientID
fleek applications create
#✔ Enter the name of the new application: …
# Enter the application name, it can be anything.
#✔ Enter one or more domain names to whitelist, separated by commas (e.g. example123.com, site321.com) …
# Enter the website domain, i.e., the domain of the static deployment on Fleek. It is recommended to configure localhost and 127.0.0.1, as it will involve cross-origin issues.
# Get the ClientID
fleek applications list
After obtaining the ClientID, you can add it to the environment variables. When uploading to local IPFS from the front-end page, you can also upload it to Fleek. Below is a simple example.
// npm install @fleek-platform/sdk
import { FleekSdk, ApplicationAccessTokenService } from "@fleek-platform/sdk";
// Configure VITE_FLEEK_CLIENT in environment variables
const applicationService = new ApplicationAccessTokenService({
clientId: import.meta.env.VITE_FLEEK_CLIENT,
});
const fleekSdk = new FleekSdk({
accessTokenService: applicationService,
});
// The IPFS storage in Fleek.xyz defaults to using CID v1, which will start with ba
// While Kubo's default CID is v0, which starts with Qm. To maintain consistency, set Kubo's default CID version to v1.
const uploadOpmlToIpfs = async () => {
try {
const xml = buildOpml(opml);
const res = await kuboClient?.add(xml, { cidVersion: 1 });
await fleekSdk.ipfs().add({
path: res.path,
content: xml,
});
addAlert(`OPML uploaded to IPFS`, "success");
} catch (error) {
console.error("Can't upload OPML to IPFS:", error);
}
};
To enhance the reliability of IPFS files, it is still recommended to upload IPFS files to different IPFS service providers simultaneously, as in my experience, relying solely on Fleek's IPFS storage service still results in slow file retrieval speeds.
Some Development Insights#
Initially, the idea was to develop a tool-type Dapp where the interaction logic would be handled by smart contracts, and data storage would be managed by IPFS. Both services are decentralized, and I wouldn't need to set up servers or provide maintenance. I thought that once developed, it would require minimal maintenance, making it a one-time project. However, with the introduction of Fleek and some centralized service API keys, I realized that even for a simple tool-type project, a certain level of maintenance is still required, and the reliability of some features still depends on centralized platforms. A purely decentralized Dapp is still difficult to achieve at present.
During development, I also wondered whether the high transaction fees of the blockchain would be a barrier for tool-type Dapps, as who would want to pay dozens of dollars just to store a simple OPML file? However, the reality is that various Layer 2 chains have already optimized transaction fees to just a few cents. The remaining $1 after playing with cross-chain bridges is enough for me to deploy contracts and use the Dapp normally without any pain (perhaps I should store the OPML file directly on the blockchain?). This has truly amazed me with the excellent experience brought by blockchain development over the past two years.
Another major realization is that IPFS is indeed slow and unpredictable, greatly affecting user experience. To ensure a good user experience, it's better to use cloud servers instead of struggling with IPFS, as it completely conflicts with user experience.
Related Links#
RainbowKit Documentation
Wagmi Documentation
Web3.js Documentation
Apply for WalletConnect projectId
js-kubo-rpc-client GitHub
Remix IDE Online Smart Contract Development
INFURA Sepolia Test Token Faucet
Fleek.co Static Frontend Page Deployment
Fleek.xyz IPFS Storage Documentation