LinSoap

LinSoap

Null
github
x

ツールクラス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);
              }}>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("Kuboゲートウェイに接続できません:", 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("IPFSにOPMLをアップロードできません:", 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("IPFSからOPMLをインポートできません:", error);
  }
};

フロントエンド静的ファイルのデプロイ#

React+Vite を使用してフロントエンドページを作成した後、Vite で静的ファイルをコンパイルし、IPFS Desktop を使用してコンパイルされた dist フォルダにアップロードし、ローカルに固定します。これで、この Dapp は分散型ネットワークにデプロイされましたが、現時点での可用性はほとんどありません。コンパイルされた静的ファイルはローカルでのみ使用可能で、他の環境での可用性は完全に運次第です。最終的には、中央集権と分散型の間で妥協点を見つけ、Fleek サービスを使用しました。
Fleek は Vercel のような自動デプロイ、静的ウェブサイトホスティングプラットフォームですが、静的ウェブサイトを 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がIPFSにアップロードされました`, "success");
  } catch (error) {
    console.error("IPFSにOPMLをアップロードできません:", error);
  }
};

IPFS ファイルの信頼性を高めるために、異なる IPFS サービスプロバイダーに同時に IPFS ファイルをアップロードすることをお勧めします。私の使用感からすると、Fleek の IPFS ストレージサービスのみを使用していると、ファイル取得速度は依然として遅いです。

開発の感想#

最初は、ツール系の Dapp を開発した後、インタラクションロジックはすべてスマートコントラクトに任せ、データストレージは IPFS に任せるという構想を持っていました。これら 2 つのサービスはすべて分散化されており、サーバーを設置したり運用を提供したりする必要がなく、開発後はほとんどメンテナンスが不要なプロジェクトになると思っていました。しかし、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 ストレージドキュメント

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