LinSoap

LinSoap

Null

ツールクラスDappの開発プロセスを記録する

最近開発されたツール系の Dapp で、主な内容はユーザーが RSS Feed の OPML ファイルをアップロードし、IPFS にアップロードして CID を生成し、その後スマートコントラクトを使用してウォレットアドレスと CID のマッピングを行い、ウェブページ上で簡単なファイル操作をサポートすることです。目標は、各ユーザーがこの Dapp を通じて自分の opml ファイルを分散型で維持できるようにすることです。しかし、このようなシンプルな目標でも、開発中に多くの問題に直面しましたので、ブログを書いて記録しておきます。
(私はまだ在校生で、興味から Dapp 開発に触れ始めたばかりで、技術力は限られています)

使用技術スタック#

  • React+Vite
    フロントエンドの静的ページを構築します。
  • RainbowKit
    汎用のウォレット接続コンポーネントで、ウォレット接続、ブロックチェーンの切り替え、ウォレット情報の確認を簡単にカスタマイズできます。
  • Wagmi
    Ethereum ウォレットとブロックチェーンとのインタラクションに使用される React Hooks ライブラリ。ウォレットの接続、オンチェーンデータの取得、コントラクトの呼び出しなどの機能を提供します。
  • Web3.js
    スマートコントラクトを呼び出し、ブロックチェーンとのインタラクションを実現します。
  • Kubo-rpc-client
    IPFS ノードとの通信に使用されるクライアントライブラリ。IPFS の RPC インターフェースを介して、ファイルのアップロード、取得、固定を行います。
  • Remix IDE
    スマートコントラクトの作成とデプロイに使用され、シンプルなスマートコントラクト開発に適しています。
  • fast-xml-parser
    OPML ファイルを解析および構築するために使用され、OPML に対して基本的な追加、削除、更新操作を行うことができます。

Dapp 実現目標#

ユーザーが使用を開始する際には、3 つの初期化ステップを行う必要があります。第一ステップはウォレットの接続、第二ステップは Kubo ゲートウェイへの接続、第三ステップは OPML ファイルのインポートで、ローカルまたは CID を介して IPFS からインポートをサポートします。この CID はブロックチェーンのマップ情報から自動的に取得できます。
初期化ステップが完了すると、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 component.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);
              }}>Update IPFS Address<button/>
            <button onClick={() => {
                getIPFSAddress(address, chainId);
              }}>Get IPFS Address<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 リポジトリに接続し、デプロイ設定を構成し、一クリックでデプロイできます。その後、Custom Domains で自分のドメインをバインドするだけです。Fleek は ENS と HNS もサポートしています。

IPFS ファイルの信頼性保証#

Kubo-rpc-client を使用してアップロードされた Opml ファイルは、Kubo に固定され、同じ Kubo サービスを介してファイルの速度と信頼性は良好です。しかし、異なるネットワーク環境では、その信頼性も一言では言えません。多くの場合、ブロックチェーンのマッピングを通じて CID を取得しましたが、CAT ファイルの取得には非常に長い時間がかかり、失敗することもあります。この時、IPFS サービスプロバイダーが必要になります。IPFS ストレージサービスを提供するプロバイダーは多数あり、例えば以下のようなものがあります。

  • Panata
  • Filebase
  • Infura
  • Fleek

これらのサービスプロバイダーは、いくつかの無料ストレージ枠を提供しており、少ないもので数 GB、多いもので十数 GB です。OPML のような純粋なテキストファイルのストレージには、ユーザー数が少ない場合に十分対応できます。
しかし、Panata と Filebase の SDK はバックエンド環境でのファイルアップロードのみをサポートしており、純粋な静的ページではそのサービスを使用できません。また、これらの 2 社は無料アカウントに対して HTML ファイルのストレージサービスを提供していないため、OPML を保存しても HTML として認識されて保存できません。Infura については、無料アカウントでの IPFS ストレージ権限の取得方法が不明です(研究が不十分で、実現方法があれば教えてください)。
最終的には Fleek の IPFS ストレージサービスを選択しましたが、Fleek には fleek.co と fleek.xyz の 2 つのバージョンのウェブサイトがあるようで、私のフロントエンドページは fleek.co にデプロイされていますが、IPFS ストレージサービスは fleek.xyz にしかありません。

Fleek の IPFS ストレージサービスを使用するには、ClientID を申請する必要があり、ClientID は Fleek の Cli で申請する必要があるため、Fleek の SDK をインストールする必要があります。

# Nodejs >= 18.18.2が必要です
npm install -g @fleek-platform/cli

# Fleekアカウントにログイン
fleek login

# ClientIDを作成
fleek applications create

#✔ 新しいアプリケーションの名前を入力してください: …
#アプリケーション名を入力します。任意で構いません。

#✔ ホワイトリストにするドメイン名を1つ以上入力してください。カンマで区切ってください(例: 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 キーの導入により、たとえロジックがシンプルなツール系プロジェクトであっても、一定のメンテナンスが必要であることがわかりました。一部の機能の信頼性は依然として中央集権プラットフォームに依存する必要があります。完全に分散型の Dapp は現在のところ実現が難しいです。
開発中には、ブロックチェーンの高額な取引手数料がツール系 Dapp の障壁ではないかと考えていました。結局、誰もが OPML ファイルを保存するのに数十ドルも支払いたくはないでしょう。しかし、現実にはさまざまな Layer2 チェーンが取引手数料を数セントに最適化できるようになっています。クロスチェーンブリッジを利用して残った 1 ドルでコントラクトを公開し、Dapp を正常に使用できることができました。全く痛みを感じませんでした(もしかしたら OPML ファイルを直接ブロックチェーンに保存すべきかもしれません?)。この点は、ここ 2 年のブロックチェーンの発展による素晴らしい体験に驚かされました。
また、最大の感想は、IPFS は本当に遅くて不確実であり、ユーザー体験に大きな影響を与えるため、ユーザー体験を保証するためにはクラウドサーバーを使用するべきであり、IPFS をいじくり回すべきではないということです。この技術は完全にユーザー体験と対立しています。

関連リンク#

RainbowKit ドキュメント
Wagmi ドキュメント
Web3.js ドキュメント
WalletConnect projectId の申請
js-kubo-rpc-client Github
Remix IDE オンラインスマートコントラクト開発
INFURA Sepolia テストコインの水道
Fleek.co 静的フロントエンドページのデプロイ
Fleek.xyz IPFS ストレージドキュメント

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。