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
To build the front-end static pages. - RainbowKit
A universal wallet connection component that allows easy customization of wallet connections, blockchain switching, and viewing wallet information. - 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. It allows uploading files, retrieving 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 building 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 files or via CID from IPFS. This CID can be automatically obtained through the blockchain mapping 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 write the new CID to the blockchain by calling the smart contract.
Smart Contract Part#
The logic of the smart contract for this project is very simple; it maintains a mapping table of wallet addresses to CIDs and provides query and update operations. For simple smart contracts, you can directly write and deploy them using Remix IDE. This eliminates most of the hassle of setting up the environment, making it very convenient to deploy contracts to various blockchains using Remix IDE.
Here 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, compile it to obtain a generic ABI file. Using the web3.js library, you can implement the same interaction logic across various chains by using this ABI file along with the actual deployed addresses of different chains. Deploying the contract on various blockchains will yield different contract addresses. In the React project, maintain a mapping table of contract addresses so that the Dapp can call different smart contracts on different chains.
For this project, the confirmation speed of the blockchain is not very critical. Since there may be many operations, you can choose some Layer 2 blockchains to significantly reduce the costs of contract interactions, or use free testnets.
Frontend Contract Interaction Part#
The front-end contract interaction part mainly uses the RainbowKit and Wagmi libraries. RainbowKit provides a customizable wallet connection component that essentially covers all 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 component.
When using RainbowKit's components, you need to apply for a projectId from WalletConnect Cloud and configure it in RainbowkitConfig. By using the Provider that includes the current component, you can 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 obtain the wallet address. Wagmi provides many blockchain-related hooks to check the user address, view the current chain ID, and other operations.
Here 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
// For specific blockchain IDs, refer to 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 the Go language, and common IPFS Desktop applications and the IPFS service built into the Brave browser are all 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 configured; otherwise, you will not be able to 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"
]
}
},
...
}
Here is a simple example of creating a Kubo-rpc-client instance and performing upload and download operations.
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 default pin parameter for the add method is true, meaning that the file will be pinned to IPFS after uploading. For more parameters, refer to
// 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);
}
};
// Retrieve a file from IPFS
// Since Cat in IPFS is an asynchronous iterator, a utility method is written to retrieve 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 using React + Vite to complete the front-end page development, compile the static files using Vite, 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 name resolution functionality, allowing you to avoid maintaining IPNS for the front-end static files locally and using long and unwieldy CID addresses for access.
The usage is similar to Vercel; connect to the GitHub repository, configure the Deploy settings, and deploy with one click. Then bind your own domain name in Custom Domains. Fleek also supports ENS and HNS.
IPFS File Reliability Assurance#
The OPML files uploaded using Kubo-rpc-client will be pinned on Kubo, and the speed and reliability of retrieving files from the same Kubo service are acceptable. However, the reliability in different network environments is also questionable. Many times, I have already obtained the CID through blockchain mapping, but retrieving the file can take an extremely long time or even fail. At this point, IPFS service providers are still needed. There are many service providers offering IPFS storage services, 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 files, especially when the user base is small.
However, the SDKs of Panata and Filebase only support file uploads in backend environments, making it impossible for pure static pages to use their services. Another significant drawback 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 in-depth, so if there is an implementation plan, please point it out).
Ultimately, I chose Fleek's IPFS storage service, but it seems that Fleek has two versions of its 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 name, which is the domain name of the front-end 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 to Fleek. Here 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 version, which will start with ba
// While the default CID generated by Kubo is v0 version, which starts with Qm. To maintain consistency, set the default CID version of Kubo 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#
At the beginning, 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 of these services are decentralized, and I wouldn't need to set up servers or provide maintenance. I thought that once developed, it would require little to no maintenance, making it a one-time effort project. However, with the introduction of Fleek and some centralized service API keys, I found 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 on the blockchain would be a barrier for tool-type Dapps, as who wants 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 from playing with cross-chain bridges is enough for me to deploy contracts and use the Dapp normally without any pain (perhaps I should just store the OPML file directly on the blockchain?). This has truly amazed me with the excellent experience brought by the development of blockchain over the past two years.
Another major realization is that IPFS is indeed slow and unpredictable, which greatly affects user experience. To ensure 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 Testnet Faucet
Fleek.co Static Frontend Page Deployment
Fleek.xyz IPFS Storage Documentation