最近開發了工具類的 Dapp,主要內容就是用戶上傳 RSS Feed 的 OPML 文件,上傳到 IPFS,生成一個 CID,然後使用智能合約,對錢包地址和 CID 做一個映射,支持在網頁端進行一個簡單的文件操作。目標是希望每個用戶都可以通過這個 Dapp 去中心化的維護一份自己的 opml 文件。但是這樣一個簡單的目標,在開發的時候也遇到了很多的問題,開一篇博客記錄一下,以供參考。
(本人還只是在校學生,憑興趣接觸 Dapp 開發不久,技術力有限)
所用技術棧#
- React+Vite
構建前端靜態頁面。 - RainbowKit
通用的錢包連接組件,輕鬆自定義錢包連接,區塊鏈切換,查看錢包信息。 - Wagmi
用於與以太坊錢包和區塊鏈進行交互的 React Hooks 庫。提供了簡單的接口來連接錢包、獲取鏈上數據、調用合約等功能。 - Web3.js
用於調用智能合約,實現與區塊鏈的交互。 - Kubo-rpc-client
用於與 IPFS 節點進行通信的客戶端庫。通過 IPFS 的 RPC 接口,上傳文件、獲取文件、固定文件。 - Remix IDE
用於編寫和部署智能合約,適合簡單的智能合約開發。 - fast-xml-parser
用於解析和構建 OPML 文件,使得能夠對 OPML 進行基本的添加、刪除、更新操作。
Dapp 實現目標#
在用戶開始使用時,需要進行 3 個初始化步驟,第一步是連接錢包,第二步是連接 Kubo 網關,第三步是導入 OPML 文件,支持從本地或通過 CID 從 IPFS 導入,此 CID 可以通過區塊鏈 Map 的信息自動獲取。
在完成初始化步驟後,即可對 OPML 文件進行一些基礎的操作。在完成操作後,用戶將文件重新上傳到 IPFS,獲取新的 CID,最後通過調用智能合約將新的 CID 寫入區塊鏈。
智能合約部分#
該項目的智能合約邏輯非常簡單,就是維護一個錢包地址對 CID 的映射表,提供查詢和更新操作即可。對於簡單的智能合約直接使用 Remix IDE 編寫和部署即可。可以免去大部分安裝環境的麻煩,使用 Remix IDE 將合約部署到各個區塊鏈上非常的方便。
以下是我編寫的 sol 合約
//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];
}
}
完成智能合約編寫後進行編譯,獲取到一個通用的 ABI 文件,通過 web3.js 庫,根據這個 ABI 文件配合不同鏈實際部署的地址,即可實現在各種鏈上完成相同的交互邏輯,將合約發布到各個區塊鏈上,可以獲得不同的合約地址,在 React 項目中維護一個合約地址映射表,使得 Dapp 可以選擇不同的鏈調用不同的智能合約。
對於這個項目而言,區塊鏈的確認速度不是很關鍵,操作次數可能會比較多,可以選擇一些 Layer2 的區塊鏈,大大降低合約交互的費用,或者使用免費的測試網。
前端合約交互部分#
前端合約交互部分主要使用到 RainbowKit 和 Wagmi 庫,RainbowKit 提供了一個可自定義的錢包連接組件,基本上提供了連接錢包所有的交互邏輯,包括連接錢包,查看錢包信息,切換錢包當前鏈。開發者只需要在默認組件上做樣式修改即可。
在使用 RainbowKit 的組件的時候,需要到 WalletConnect Cloud 申請一個 projectId,配置到 RainbowkitConfig 中,使用 Provider 包括當前組件即可在項目中使用 RainbowKit hook。
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>
);
};
智能合約的調用可以使用 web3.js 庫,通過提供 ABI 文件和合約地址,即可調用智能合約,想要調用以上的智能合約的 getIPFSHash 方法,需要傳入一個錢包地址,此時就可以使用 Wagmi Hooks 獲取錢包地址,Wagmi 提供了很多區塊鏈相關的 Hook,可以查看用戶地址,查看當前鏈 ID 等操作。
以下是一個簡單的 web3.js 調用智能合約的實例。
//Example React Provider componet.ts
import contractABI from "./RSSFeedStorage.json";
import Web3 from "web3";
//定義不同鏈的合約地址
//具體區塊鏈ID查看https://wagmi.sh/react/api/chains#supported-chains
const contractAddresses: { [key: number]: { address: string } } = {
11155111: { // Sepolia
address: "0x63Bbcd45b669367034680093CeF5B8BFEee62C4d"
},
10: { // Optimism
address: "0x2F4508a5b56FF1d0CbB2701296C6c3EF8eE7D6B5"
},
};
//調用合約updateIPFSHash方法
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;
};
//調用合約getIPFSHash方法
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;
};
在 react 組件中,使用 Wagmi Hook 調用以上方法
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);
}}>更新 IPFS 地址<button/>
<button onClick={() => {
getIPFSAddress(address, chainId);
}}>獲取 IPFS 地址<button/>
</>
)
}
export default Example
IPFS 存儲部分#
IPFS 存儲部分主要使用 Kubo-rpc-client 庫,Kubo 是 IPFS Golang 語言的實現方案,像是常見的 IPFS Desktop,以及 Brave 瀏覽器自帶的 IPFS 服務均為 Kubo, 使用 Kubo-rpc-client 庫連接 Kubo 網關後,即可輕鬆上傳,下載,固定文件。
注意:在 Kubo 的配置文件中,要進行跨域設置,否則無法連接到 Kubo 網關。
{
"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"
]
}
},
...
}
以下是一個簡單的創建 Kubo-rpc-client 實例,並進行上傳,下載的示例。
import { create, KuboRPCClient } from "kubo-rpc-client";
// 連接Kubo網關,創建kubo-rpc-client實例
const connectKubo = async (KuboUrl: string) => {
try {
const KuboClient = create({ KuboUrl });
} catch (error) {
console.error("Can't connect to Kubo gateway:", error);
throw error;
}
};
// 上傳文件到IPFS
const uploadOpmlToIpfs = async (opml) => {
try {
const buildedOpml = buildOpml(opml);
//add方法pin參數默認為true,即上傳文件後默認固定到IPFS,更多參數查看
//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);
}
};
// 從IPFS獲取一個文件
// 由於IPFS中的Cat是一個異步迭代器,編寫一個工具方法獲取完整數據。
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;
}
// 調用方法,把二進制數據解碼為文本。
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);
}
};
前端靜態文件部署#
在使用 React+Vite 完成前端頁面編寫後,使用 Vite 編譯靜態文件,使用 IPFS Desktop 上傳到編譯的 dist 文件夾並固定到本地即可。至此為此,該 Dapp 已經部署到去中心化網絡中,但是此時的可用性只能說是一點沒有,編譯的靜態文件除了在本地可用,在其他環境中可用性完全看運氣,最後還是在中心化和去中心化找了一個折中點,使用了 Fleek 服務。
Fleek 是一個類似 Vercel 的自動化部署、靜態網站托管平台,但是 Fleek 會將靜態網站發布到 IPFS 中,並且自帶了 CDN, 對於 IPFS 環境的瀏覽器,提供 IPFS 服務,對於沒有 IPFS 環境的瀏覽器,同樣提供服務,使得 Dapp 的可用性大大增加,最重要的是,提供了 web2 域名解析的功能,配合 CloudFlare 服務,不用在本地為該前端靜態文件維護 IPNS,以及使用又臭又長的 CID 地址訪問了。
使用方法和 Vercel 類似,連接到 Github 倉庫,配置 Deploy 設置,一鍵部署即可。然後在 Custom Domains 綁定上自己的域名即可。Fleek 同樣支持 ENS 與 HNS。
IPFS 文件可靠性保證#
在使用 Kubo-rpc-client 上傳的 Opml 文件,會固定在 Kubo 上,通過同一個 Kubo 服務獲取文件速度和可靠性還行。但是在不同的網絡環境中,這可靠性同樣一言難盡,在很多時候已經通過區塊鏈的映射獲取到了 CID,但是 CAT 文件要非常非常久,甚至失敗。這時候還是會需要用到 IPFS 服務商,提供 IPFS 存儲的服務商有很多,例如
- Panata
- Filebase
- Infura
- Fleek
這些服務商都有一定的免費存儲額度,少的幾個 g, 多的十來 g, 對於存儲 OPML 這種純文本文件,在用戶量較小的時候足以輕鬆應對。
但是 Panata 和 Filebase 的 SDK 只支持後端環境上傳文件,導致純靜態頁面無法使用其服務,還有一個巨坑的點,這兩家服務商對於免費賬戶不提供存儲 html 文件服務,但是我存的是 OPML,他也會識別成 html 不讓我存儲,Infura 還不清楚免費賬戶如何開通 IPFS 存儲權限(研究的不深入,如果有實現方案歡迎指出)
最後還是選擇了 Fleek 的 IPFS 存儲服務,但是 Fleek 貌似有兩個版本的網站,一個是 fleek.co,一個是 fleek.xyz,我的前端頁面是部署在 fleek.co 上,但是 ipfs 存儲服務只有 fleek.xyz 上才有。
使用 fleek 的 IPFS 存儲服務需要申請一個 ClientID,ClientID 需要在 Fleek 的 Cli 中申請,所以需要安裝 Fleek 的 SDK。
# You need to have Nodejs >= 18.18.2
npm install -g @fleek-platform/cli
#登錄fleek賬戶
fleek login
#創建ClientID
fleek applications create
#✔ Enter the name of the new application: …
#輸入應用名稱,隨意即可。
#✔ Enter one or more domain names to whitelist, separated by commas (e.g. example123.com, site321.com) …
#輸入網站域名,即fleek前端靜態部署的域名,建議配置上localhost和127.0.0.1,會涉及到跨域問題。
#獲取ClientID
fleek applications list
在獲取到 ClientID 後,可以在環境變量中添加 ClientID, 在前端頁面上傳到本地 IPFS 時,同時上傳到 Fleek。以下是一個簡單示例。
// npm install @fleek-platform/sdk
import { FleekSdk, ApplicationAccessTokenService } from "@fleek-platform/sdk";
//在環境變量中配置VITE_FLEEK_CLIENT
const applicationService = new ApplicationAccessTokenService({
clientId: import.meta.env.VITE_FLEEK_CLIENT,
});
const fleekSdk = new FleekSdk({
accessTokenService: applicationService,
});
//Fleek.xyz中的IPFS存儲默認是使用CID v1版本,生成的cid會以ba開頭
//而Kubo默認生成的CID為v0版本,會以Qm開頭,為保持一致,將kubo默認CID版本設置為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);
}
};
為了增加 IPFS 文件的可靠性,還是建議將 IPFS 文件同時上傳到不同的 IPFS 服務商,因為在我的使用感受中,只使用 Fleek 的 ipfs 存儲服務,獲取文件速度仍然感人。
一點開發感受#
在一開始的時候,設想的是開發一個工具類 Dapp 後,交互邏輯都交給智能合約,數據存儲交給 IPFS, 這兩項服務都是去中心化的,自己也不需要架設服務器、提供運維,想着開發出來後基本上就不需要再維護了,屬於一勞永逸的項目。但隨著 Fleek 和一些中心化服務的 api key 的引入,發現就算是邏輯簡單的工具類項目,同樣需要提供一定限度的維護,有些功能的可靠性仍然需要中心化平台提供。一個純粹的 Dapp 在目前仍然難以實現。
在開發的時候,還想着區塊鏈高額的交易費是不是工具類 Dapp 的一個門檻,畢竟誰想簡單的存個 OPML 文件要付費幾十刀。但是現實是各種 Layer2 鏈已經能夠將交易費優化到幾美分。玩跨鏈橋剩下的 1 美元足夠我發布合約並且正常使用該 Dapp 了,毫不肉痛(或許我應該把 OPML 文件直接存在區塊鏈上?)。這點還是讓我驚嘆這兩年區塊鏈發展帶來的優秀體驗。
還有一個最大的感受就是 IPFS 真的是又慢又玄學,極大影響用戶體驗,為保證用戶體驗還是上雲服務器吧,別折騰 IPFS 了,這玩意完全和用戶體驗衝突。
相關鏈接#
RainbowKit 文檔
Wagmi 文檔
Web3.js 文檔
申請 WalletConnect projectId
js-kubo-rpc-client Github
Remix IDE 在線智能合約開發
INFURA Sepolia 測試幣水龍頭
Fleek.co 靜態前端頁面部署
Fleek.xyz IPFS 存儲文檔