NEROChainコミュニティに参加して、今後の情報をお待ちください!
クックブック低レベル統合最初のdAppを作成する

NERO Chainでの最初のdApp作成

このレシピでは、アカウント抽象化(Account Abstraction)を使用してNERO Chain上に最初の分散型アプリケーション(dApp)を構築する手順を説明します。ユーザーがガス料金のためのNEROトークンを必要とせずにNFTをミントできる簡単なNFTミンティングアプリケーションを作成します。

学習内容

  • アカウント抽象化を使用した完全なReact dAppのセットアップ方法
  • ガスレストランザクションによるシームレスなNFTミント体験の実装方法
  • トランザクションガス料金の複数の支払いオプションの提供方法
  • ウォレット接続からトランザクション確認までの完全なユーザージャーニーの処理方法
  • 前回のチュートリアルからすべての概念を組み合わせて一貫したアプリケーションを作成する方法

前提条件

ステップ1:開発環境のセットアップ

開始する最も簡単な方法は、NERO ChainとAccount Abstractionのために事前設定されたテンプレートリポジトリを使用することです。

# テンプレートリポジトリをクローン
git clone https://github.com/nerochain/application-templates-nero my-first-dapp
cd my-first-dapp/react-ts/
 
# 依存関係をインストール
npm install
 
# 環境ファイルを作成
cp .env.example .env

注意: 最初からReactプロジェクトを作成することもできますが、上記のリポジトリをクローンした方が簡単です。

.envファイルを開き、PaymasterのAPIキーを追加します:

REACT_APP_PAYMASTER_API_KEY=aa_platformから取得したAPIキー

また、NFTコントラクトアドレスも設定する必要があります。このレシピでは、NERO Chainテストネット上の例NFTコントラクトを使用できます:

REACT_APP_NFT_CONTRACT=あなたのNFTコントラクトアドレス

ステップ2:プロジェクト構造の理解

テンプレートは整理された構造を提供します:

my-first-dapp/
├── public/                  # 静的アセット
├── src/
│   ├── components/          # Reactコンポーネント
│   │   ├── WalletConnect.tsx     # ウォレット接続コンポーネント
│   │   ├── NFTMinter.tsx         # NFTミントコンポーネント
│   │   └── PaymentTypeSelector.tsx  # 支払い選択コンポーネント
│   ├── utils/
│   │   ├── aaUtils.ts       # アカウント抽象化ユーティリティ
│   │   └── errorHandler.ts  # エラー処理ユーティリティ
│   ├── App.tsx              # メインアプリケーションコンポーネント
│   ├── config.ts            # 設定
│   └── index.tsx            # アプリケーションのエントリポイント
└── package.json             # プロジェクトの依存関係

ステップ3:ウォレット接続の実装

テンプレートにはプレースホルダー実装が含まれています。まず、ウォレット接続機能を実装しましょう。

src/utils/aaUtils.tsを開き、getSigner関数を更新します:

// プレースホルダー実装を置き換え
export const getSigner = async () => {
  if (!window.ethereum) {
    throw new Error("暗号ウォレットが見つかりません。MetaMaskをインストールしてください。");
  }
  
  try {
    // アカウントへのアクセスをリクエスト
    await window.ethereum.request({ method: 'eth_requestAccounts' });
 
    // プロバイダーとサイナーを作成
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    const signer = provider.getSigner();
 
    // アドレスを取得してサイナーを検証
    const address = await signer.getAddress();
    console.log("接続されたウォレットアドレス:", address);
 
    return signer;
  } catch (error) {
    console.error("ウォレットへの接続エラー:", error);
    throw error;
  }
};

また、getAAWalletAddress関数も実装します:

export const getAAWalletAddress = async (accountSigner: ethers.Signer) => {
  try {
    // getAddressメソッドを持つ有効なサイナーであることを確認
    if (!accountSigner || typeof accountSigner.getAddress !== 'function') {
      throw new Error("無効なサイナーオブジェクト: getAddressメソッドが必要です");
    }
    
    // SimpleAccountビルダーを初期化
    const simpleAccount = await Presets.Builder.SimpleAccount.init(
      accountSigner,
      NERO_CHAIN_CONFIG.rpcUrl,
      {
        overrideBundlerRpc: AA_PLATFORM_CONFIG.bundlerRpc,
        entryPoint: CONTRACT_ADDRESSES.entryPoint,
        factory: CONTRACT_ADDRESSES.accountFactory,
      }
    );
    
    // AAウォレットのcounterfactualアドレスを取得
    const address = await simpleAccount.getSender();
    console.log("AAウォレットアドレス:", address);
    
    return address;
  } catch (error) {
    console.error("AAウォレットアドレス取得エラー:", error);
    throw error;
  }
};

次に、WalletConnectコンポーネントでウォレット接続を適切に実装しましょう。src/components/WalletConnect.tsxを開き、ウォレットの状態変更を処理するように更新します:

import React, { useState, useEffect } from 'react';
import { getSigner, getAAWalletAddress } from '../utils/aaUtils';
import { ethers } from 'ethers';
 
interface WalletConnectProps {
  onWalletConnected?: (eoaAddress: string, aaAddress: string) => void;
}
 
const WalletConnect: React.FC<WalletConnectProps> = ({ onWalletConnected }) => {
  const [isConnected, setIsConnected] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [eoaAddress, setEoaAddress] = useState('');
  const [aaAddress, setAaAddress] = useState('');
  const [error, setError] = useState<string | null>(null);
  
  // コンポーネントマウント時にウォレットが既に接続されているかチェック
  useEffect(() => {
    const checkWalletConnection = async () => {
      try {
        if (window.ethereum) {
          const accounts = await window.ethereum.request({ method: 'eth_accounts' });
          if (accounts && accounts.length > 0) {
            await connectWallet();
          }
        }
      } catch (error) {
        console.error("ウォレット接続確認エラー:", error);
      }
    };
    
    checkWalletConnection();
    
    // アカウント変更をリッスン
    if (window.ethereum) {
      window.ethereum.on('accountsChanged', (accounts: string[]) => {
        if (accounts.length === 0) {
          disconnectWallet();
        } else {
          connectWallet();
        }
      });
    }
    
    return () => {
      // イベントリスナーをクリーンアップ
      if (window.ethereum) {
        window.ethereum.removeListener('accountsChanged', () => {});
      }
    };
  }, []);
 
  const connectWallet = async () => {
    try {
      setIsLoading(true);
      setError(null);
      
      // ウォレットからサイナーを取得
      const signer = await getSigner();
      if (!signer) {
        throw new Error("ウォレットからサイナーの取得に失敗しました");
      }
      
      // EOAアドレスを取得
      const address = await signer.getAddress();
      setEoaAddress(address);
      
      // AAウォレットアドレスを取得
      const aaWalletAddress = await getAAWalletAddress(signer);
      setAaAddress(aaWalletAddress);
      
      // 状態を更新
      setIsConnected(true);
      
      // コールバックがあれば呼び出し
      if (onWalletConnected) {
        onWalletConnected(address, aaWalletAddress);
      }
      
    } catch (error: any) {
      console.error("ウォレット接続エラー:", error);
      setError(error.message || "ウォレットの接続に失敗しました");
    } finally {
      setIsLoading(false);
    }
  };
  
  // コンポーネントの残りの部分...
};

次に、App.tsxを更新してウォレット接続を適切に処理します:

import React, { useState } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import WalletConnect from './components/WalletConnect';
import NFTMinter from './components/NFTMinter';
import { ethers } from 'ethers';
import { getSigner } from './utils/aaUtils';
import './App.css';
 
const App: React.FC = () => {
  // ウォレット接続を追跡する状態
  const [signer, setSigner] = useState<ethers.Signer | undefined>(undefined);
  const [eoaAddress, setEoaAddress] = useState<string>('');
  const [aaAddress, setAaAddress] = useState<string>('');
  
  /**
   * ウォレット接続のハンドル - 本物のサイナーを取得することが重要!
   */
  const handleWalletConnected = async (eoaAddr: string, aaAddr: string) => {
    try {
      // ウォレットから実際のサイナーを取得 - モックサイナーは使用しないでください!
      const realSigner = await getSigner();
      
      setEoaAddress(eoaAddr);
      setAaAddress(aaAddr);
      setSigner(realSigner);
      
      toast.success('ウォレットが正常に接続されました!');
    } catch (error) {
      console.error("サイナー取得エラー:", error);
      toast.error('ウォレットサイナーの取得に失敗しました。もう一度お試しください。');
    }
  };
  
  // コンポーネントの残りの部分...
};

ステップ4:Paymaster統合のセットアップ

次に、initAABuilder関数を更新してペイマスター統合を実装します:

export const initAABuilder = async (accountSigner: ethers.Signer, apiKey?: string) => {
  try {
    // getAddressメソッドを持つ有効なサイナーであることを確認
    if (!accountSigner || typeof accountSigner.getAddress !== 'function') {
      throw new Error("無効なサイナーオブジェクト: getAddressメソッドが必要です");
    }
 
    // サイナーのアドレスを取得して動作していることを確認
    const signerAddress = await accountSigner.getAddress();
    console.log("アドレス用のAAビルダーを初期化中:", signerAddress);
    
    // SimpleAccountビルダーを初期化
    const builder = await Presets.Builder.SimpleAccount.init(
      accountSigner,
      NERO_CHAIN_CONFIG.rpcUrl,
      {
        overrideBundlerRpc: AA_PLATFORM_CONFIG.bundlerRpc,
        entryPoint: CONTRACT_ADDRESSES.entryPoint,
        factory: CONTRACT_ADDRESSES.accountFactory,
      }
    );
    
    // ペイマスター用のAPIキーを設定
    const currentApiKey = apiKey || API_KEY;
    
    // APIキーでペイマスターオプションを設定
    builder.setPaymasterOptions({
      apikey: currentApiKey,
      rpc: AA_PLATFORM_CONFIG.paymasterRpc,
      type: "0" // デフォルトはフリー(スポンサー付きガス)
    });
    
    // UserOperationのガスパラメータを設定
    builder.setCallGasLimit(300000);
    builder.setVerificationGasLimit(2000000);
    builder.setPreVerificationGas(100000);
    
    return builder;
  } catch (error) {
    console.error("AAビルダー初期化エラー:", error);
    throw error;
  }
};

次に、支払いタイプを設定するユーティリティを実装します:

export const setPaymentType = (builder: any, paymentType: number, tokenAddress: string = '') => {
  const paymasterOptions: any = { 
    type: paymentType.toString(),
    apikey: API_KEY,
    rpc: AA_PLATFORM_CONFIG.paymasterRpc
  };
  
  // ERC20支払いが選択されている場合、トークンアドレスを追加
  if (paymentType > 0 && tokenAddress) {
    paymasterOptions.token = tokenAddress;
  }
  
  builder.setPaymasterOptions(paymasterOptions);
  return builder;
};

ステップ5:NFTミントロジックの実装

NFTミント機能を実装しましょう。まず、操作を実行するための関数でaaUtils.tsファイルを更新します:

export const executeOperation = async (
  accountSigner: ethers.Signer,
  contractAddress: string,
  contractAbi: any,
  functionName: string,
  functionParams: any[],
  paymentType: number = 0,
  selectedToken: string = '',
  options?: {
    apiKey?: string;
    gasMultiplier?: number;
  }
) => {
  try {
    // サイナーを検証
    if (!accountSigner || typeof accountSigner.getAddress !== 'function') {
      throw new Error("無効なサイナー: getAddressメソッドがありません");
    }
    
    // クライアントを初期化
    const client = await initAAClient(accountSigner);
 
    // ペイマスターでビルダーを初期化
    const builder = await initAABuilder(accountSigner, options?.apiKey);
 
    // 支払いタイプとトークンが指定されている場合は設定
    if (paymentType > 0 && selectedToken) {
      // ERC20トークンの支払いオプションを設定
      builder.setPaymasterOptions({
        apikey: options?.apiKey || API_KEY,
        rpc: AA_PLATFORM_CONFIG.paymasterRpc,
        type: paymentType.toString(),
        token: selectedToken
      });
    } else {
      // デフォルトの支払いオプション(スポンサー付き)を設定
      builder.setPaymasterOptions({
        apikey: options?.apiKey || API_KEY,
        rpc: AA_PLATFORM_CONFIG.paymasterRpc,
        type: paymentType.toString()
      });
    }
 
    // コントラクトインスタンスを作成
    const contract = new ethers.Contract(
      contractAddress,
      contractAbi,
      getProvider()
    );
 
    // 関数呼び出しデータをエンコード
    const callData = contract.interface.encodeFunctionData(
      functionName,
      functionParams
    );
 
    // ビルダーにトランザクションデータを設定
    const userOp = await builder.execute(contractAddress, 0, callData);
 
    // ユーザーオペレーションをバンドラーに送信
    console.log("UserOperationをバンドラーに送信");
    const res = await client.sendUserOperation(userOp);
 
    console.log("UserOperationがハッシュで送信されました:", res.userOpHash);
 
    // トランザクションが含まれるのを待つ
    const receipt = await res.wait();
 
    // 利用可能な場合はトランザクションハッシュをログに記録
    if (receipt && receipt.transactionHash) {
      console.log("トランザクションがマイニングされました:", receipt.transactionHash);
    }
 
    // 操作結果を返す
    return {
      userOpHash: res.userOpHash,
      transactionHash: receipt?.transactionHash || '',
      receipt: receipt
    };
  } catch (error) {
    console.error("操作実行エラー:", error);
    throw error;
  }
};

次に、NFTミント関数を追加します:

// ファイルの先頭にNFT ABIの定義を追加
const NFT_ABI = [
  "function mint(address to, string memory uri) external",
  "function tokenURI(uint256 tokenId) external view returns (string memory)",
  "function balanceOf(address owner) external view returns (uint256)"
];
 
export const mintNFT = async (
  accountSigner: ethers.Signer,
  recipientAddress: string,
  metadataUri: string,
  paymentType: number = 0,
  selectedToken: string = '',
  options?: {
    apiKey?: string;
    gasMultiplier?: number;
  }
) => {
  try {
    // ミント関数を実行
    return await executeOperation(
      accountSigner,
      CONTRACT_ADDRESSES.nftContract,
      NFT_ABI,
      'mint',
      [recipientAddress, metadataUri],
      paymentType,
      selectedToken,
      options
    );
  } catch (error) {
    console.error("NFTミントエラー:", error);
    throw error;
  }
};

ステップ6:ガス料金のトークンサポートの追加

前払いおよび後払いオプションでは、Paymasterからサポートされているトークンを取得する必要があります。ここでは、異なるペイマスター実装が異なる形式でトークンを返す可能性があるため、問題が発生しやすい点です。以下は堅牢な実装です:

export const getSupportedTokens = async (client: any, builder: any) => {
  try {
    // ビルダーが初期化されていることを確認
    if (!builder) {
      throw new Error("ビルダーが初期化されていません");
    }
 
    // AAウォレットアドレスを取得
    const sender = await builder.getSender();
    console.log("ウォレット用のサポートされているトークンを取得:", sender);
 
    // トークン照会用の最小限のUserOpを作成
    const minimalUserOp = {
      sender,
      nonce: "0x0",
      initCode: "0x",
      callData: "0x",
      callGasLimit: "0x88b8",
      verificationGasLimit: "0x33450",
      preVerificationGas: "0xc350",
      maxFeePerGas: "0x2162553062",
      maxPriorityFeePerGas: "0x40dbcf36",
      paymasterAndData: "0x",
      signature: "0x"
    };
 
    // ペイマスターAPI呼び出し用のプロバイダーを設定
    const provider = new ethers.providers.JsonRpcProvider(AA_PLATFORM_CONFIG.paymasterRpc);
    console.log("ペイマスターRPCに接続:", AA_PLATFORM_CONFIG.paymasterRpc);
 
    // APIキーをログに記録(セキュリティのために一部を隠す)
    const maskedApiKey = API_KEY ? `${API_KEY.substring(0, 4)}...${API_KEY.substring(API_KEY.length - 4)}` : '未定義';
    console.log(`APIキーを使用: ${maskedApiKey}`);
    
    // ペイマスターAPIに対してさまざまなパラメータ形式を試す
    let tokensResponse;
    
    try {
      // 最初の形式の試み: [userOp, apiKey, entryPoint]
      console.log("pm_supported_tokensの最初のパラメータ形式を試行");
      tokensResponse = await provider.send("pm_supported_tokens", [
        minimalUserOp,
        API_KEY,
        CONTRACT_ADDRESSES.entryPoint
      ]);
    } catch (formatError) {
      console.warn("最初のパラメータ形式が失敗:", formatError);
      
      try {
        // 二番目の形式の試み: { userOp, entryPoint, apiKey }
        console.log("pm_supported_tokensの二番目のパラメータ形式を試行");
        tokensResponse = await provider.send("pm_supported_tokens", [{
          userOp: minimalUserOp,
          entryPoint: CONTRACT_ADDRESSES.entryPoint,
          apiKey: API_KEY
        }]);
      } catch (format2Error) {
        console.warn("二番目のパラメータ形式が失敗:", format2Error);
        
        // 三番目の形式の試み: { op, entryPoint }
        console.log("pm_supported_tokensの三番目のパラメータ形式を試行");
        tokensResponse = await provider.send("pm_supported_tokens", [{
          op: minimalUserOp,
          entryPoint: CONTRACT_ADDRESSES.entryPoint
        }]);
      }
    }
 
    console.log("トークンレスポンス:", tokensResponse);
 
    // 結果を変換して返す
    if (!tokensResponse) {
      console.log("トークンレスポンスが受信されませんでした");
      return [];
    }
    
    // 異なるレスポンス形式を処理
    let tokens = [];
    if (tokensResponse.tokens) {
      tokens = tokensResponse.tokens;
    } else if (Array.isArray(tokensResponse)) {
      tokens = tokensResponse;
    } else if (typeof tokensResponse === 'object') {
      // レスポンスオブジェクト内でトークンを見つける
      const possibleTokensArray = Object.values(tokensResponse).find(val => Array.isArray(val));
      if (possibleTokensArray && Array.isArray(possibleTokensArray)) {
        tokens = possibleTokensArray as any[];
      }
    }
    
    if (tokens.length === 0) {
      console.log("レスポンス内にトークンが見つかりません");
      return [];
    }
    
    // デバッグ用に生のトークンレスポンスをログに記録
    console.log("トークンレスポンスの生データ:", JSON.stringify(tokensResponse));
    
    // トークンタイプを示すレスポンス内のフラグを探す
    const isArrayWithFreepayFlag = tokens.some((t: any) => 
      'freepay' in t || 'prepay' in t || 'postpay' in t
    );
      
    if (isArrayWithFreepayFlag) {
      console.log("トークンレスポンスに支払いタイプフラグを検出");
    }
 
    const mappedTokens = tokens.map((token: any) => {
      // トークンタイプが有効な数値であることを確認
      let tokenType = 1; // デフォルトはタイプ1(前払い)
      
      // これがprepay/postpayフラグを持つレスポンスからのものかどうかを確認
      if ('freepay' in token || 'prepay' in token || 'postpay' in token) {
        if (token.freepay === true) {
          tokenType = 0; // スポンサー
        } else if (token.prepay === true) {
          tokenType = 1; // 前払い
        } else if (token.postpay === true) {
          tokenType = 2; // 後払い
        }
      } 
      // タイプが存在する場合は解析を試みる
      else if (token.type !== undefined) {
        if (typeof token.type === 'number' && !isNaN(token.type)) {
          tokenType = token.type;
        } else if (typeof token.type === 'string') {
          const parsedType = parseInt(token.type);
          if (!isNaN(parsedType)) {
            tokenType = parsedType;
          }
        }
      }
      
      // 標準化されたプロパティを持つトークンオブジェクトを作成
      return {
        address: token.token || token.address,
        decimal: parseInt(token.decimal || token.decimals || "18"),
        symbol: token.symbol,
        type: tokenType,
        price: token.price ? parseFloat(token.price) : undefined,
        // デバッグと代替フィルタリングのために元のフラグを追加
        prepay: token.prepay === true,
        postpay: token.postpay === true,
        freepay: token.freepay === true
      };
    });
 
    console.log("マッピングされたトークン:", mappedTokens);
    return mappedTokens;
  } catch (error) {
    console.error("サポートされているトークンの取得エラー:", error);
    // デバッグ用のペイマスターURLを含める
    console.error("ペイマスターURL:", AA_PLATFORM_CONFIG.paymasterRpc);
    return [];
  }
};

ステップ7:UIでのトークンフィルタリングの強化

次に、PaymentTypeSelectorコンポーネントを更新して、トークンを適切にフィルタリングして表示しましょう:

// src/components/PaymentTypeSelector.tsx
// 支払いタイプに基づいてトークンをフィルタリング
const getFilteredTokens = () => {
  if (paymentType === 0) return [];
 
  console.log("支払いタイプ:", paymentType);
  console.log("すべてのサポートされているトークン:", supportedTokens);
  
  // 各トークンが支払いタイプと一致するかどうかを確認
  // タイプを判断できない場合は、デフォルトですべてのトークンを返す
  const filtered = supportedTokens.filter(token => {
    // 正しいタイプのトークンがない場合、選択された支払いタイプのすべてのトークンを返す
    if (token.type === undefined) return true;
    
    // 数値型を一致させるために緩い等価性(==)を使用する
    return token.type == paymentType ||
      // 前払い(1)の場合、prepay=trueのトークンも含める
      (paymentType === 1 && token.prepay === true) ||
      // 後払い(2)の場合、postpay=trueのトークンも含める
      (paymentType === 2 && token.postpay === true);
  });
  
  console.log("支払いタイプ", paymentType, "に対するフィルタリングされたトークン:", filtered);
  return filtered;
};

ステップ8:エラー処理

基本的なエラー処理関数を実装するためにerrorHandler.tsファイルを更新しましょう:

export const extractErrorCode = (error: any): string | null => {
  if (!error) return null;
  
  // エラーメッセージの文字列を取得
  const errorMessage = error.message || error.toString();
  
  // AAエラーコードを抽出(形式:AA##またはFailedOp(##, "..."))
  const aaMatch = errorMessage.match(/AA(\d\d)/);
  if (aaMatch) return `AA${aaMatch[1]}`;
  
  // Paymasterエラーコードを抽出
  const pmMatch = errorMessage.match(/PM(\d\d)/);
  if (pmMatch) return `PM${pmMatch[1]}`;
  
  // FailedOp形式からエラーを抽出
  const failedOpMatch = errorMessage.match(/FailedOp\((\d+),\s*"([^"]*)"/);
  if (failedOpMatch) {
    const code = parseInt(failedOpMatch[1]);
    // コードをAAエラー形式にマッピング
    if (code >= 0 && code <= 99) {
      return `AA${code.toString().padStart(2, '0')}`;
    }
  }
  
  return null;
};
 
export const getReadableErrorMessage = (error: any): string => {
  // エラーコードを抽出
  const errorCode = extractErrorCode(error);
  
  // コードが存在する場合、マップからエラーメッセージを取得
  if (errorCode && AA_ERROR_CODES[errorCode]) {
    return `${AA_ERROR_CODES[errorCode]} (${errorCode})`;
  }
  
  // その他の一般的なイーサリアムエラーを処理
  const errorMessage = error.message || error.toString();
  
  if (errorMessage.includes("insufficient funds")) {
    return "このトランザクションを実行するための資金が不足しています";
  }
  
  if (errorMessage.includes("execution reverted")) {
    // リバート理由の抽出を試みる
    const revertMatch = errorMessage.match(/execution reverted: (.*?)($|")/);
    if (revertMatch) {
      return `トランザクションがリバートしました: ${revertMatch[1]}`;
    }
    return "トランザクションがリバートしました - ターゲットコントラクトを確認してください";
  }
  
  // 特定のエラーが特定されない場合は、元のメッセージを返す
  return errorMessage;
};

ステップ9:堅牢なサイナー処理でNFTMinterコンポーネントを実装

サイナーを適切に検証してトークン取得を処理するようにNFTMinterコンポーネントを更新します:

// src/components/NFTMinter.tsx
// コンポーネントがマウントされ、サイナーが利用可能な場合にサポートされているトークンをロード
useEffect(() => {
  const loadTokens = async () => {
    // サイナーが定義されている場合にのみ実行
    if (signer) {
      try {
        // サイナーにgetAddressメソッドがあるかどうかを確認
        if (typeof signer.getAddress !== 'function') {
          console.error("無効なサイナー: getAddressメソッドがありません");
          setError("ウォレット接続の問題: ウォレットを再接続してください");
          return;
        }
        
        // サイナーがまだ接続されているかをgetAddressを呼び出して確認
        await signer.getAddress();
        
        // 接続されている場合、トークンを取得
        fetchSupportedTokens();
      } catch (error) {
        console.error("サイナー検証エラー:", error);
        setError("ウォレット接続の問題: ウォレットを再接続してください");
      }
    } else {
      // サイナーが利用できない場合はトークンをリセット
      setSupportedTokens([]);
      console.log("サイナーが利用できないため、トークンをリセットしました");
    }
  };
  
  loadTokens();
}, [signer]);

ステップ10:dAppの実行とテスト

開発サーバーを起動します:

npm start

これにより、アプリケーションが http://localhost:3000 で開きます。dAppをテストするには:

  1. 「ウォレットを接続」ボタンを使用してウォレットを接続します
  2. NFTの名前と説明を入力します
  3. 支払いタイプを選択します:
    • 「スポンサー付き」で無料ミント
    • 「ERC20トークンで前払い」でガスを前払い
    • 「ERC20トークンで後払い」でトランザクション後にガスを支払う
  4. 前払いまたは後払いを選択した場合は、ドロップダウンからトークンを選択します
  5. 「NFTをミント」ボタンをクリックします
  6. ウォレットでトランザクションを承認します
  7. 確認を待ち、トランザクションハッシュを確認します

トークン取得の問題のトラブルシューティング

前払いまたは後払いオプションを選択したときにトークンがドロップダウンに表示されない場合:

  1. ブラウザコンソールを確認: エラーメッセージやトークンレスポンスのログを探します
  2. APIキーの確認: APIキーが有効で、トークンにアクセスする権限があることを確認します
  3. ペイマスターURLの確認: 設定ファイル内のペイマスターURLが正しいことを確認します
  4. トークンタイプの処理: getSupportedTokens関数には、さまざまなトークン形式に対する堅牢な処理が含まれています

さらに進む

この拡張されたdAppは、特にトークン取得とウォレット統合の適切な処理に注目して、NERO Chain上でのアカウント抽象化を使用した構築の重要な概念を示しています。さらに強化するために、以下のことができます:

  1. NFT表示の追加: ミントされたNFTを表示する機能を実装する
  2. トークン処理の改善: トークンの残高チェックを追加する
  3. エラー処理の強化: 再試行を含むより堅牢なエラー処理を実装する
  4. バッチトランザクションの追加: 一つのUserOperationで複数の操作をバッチ処理する実装
  5. ガス使用量の最適化: さまざまな操作のガスパラメータを微調整する

結論

おめでとうございます!アカウント抽象化を使用してNERO Chain上で最初のdAppを構築しました。学んだことは以下の通りです:

  • ユーザーのウォレットに接続し、AAウォレットを生成する方法
  • ガス料金スポンサーシップのためにNERO Chain Paymasterを統合する方法
  • UserOperationsを通じてスマートコントラクト呼び出しを実行する方法
  • ユーザーに複数の支払いオプションを提供する方法
  • トークンの取得と表示を適切に処理する方法

この基礎によって、ブロックチェーンアプリケーションにおけるガス料金の従来の障壁を取り除き、優れたユーザー体験を持つより複雑なdAppを構築できるようになります。

リソース

構築したもの

おめでとうございます!シームレスなユーザー体験のためにアカウント抽象化を活用するNERO Chain上の完全なdAppを正常に構築しました。このアプリケーションは以下を実証しています:

  1. スマートコントラクトウォレット: AAウォレットの作成と管理
  2. ガスレストランザクション: ユーザーがネイティブトークンを所有せずに操作できるようにする
  3. 複数の支払いオプション: さまざまなガス支払い方法をサポート
  4. トークン統合: ガス支払いのためのERC20トークンの使用
  5. モダンなフロントエンド: レスポンシブで直感的なUIの構築

これらの概念は、DeFi、ゲーム、ソーシャルメディアなどのより複雑なアプリケーションを作成するために拡張できます。NERO Chainのアカウント抽象化機能により、ブロックチェーン技術のすべての利点を維持しながら、従来のWebアプリケーションのように感じるユーザーフレンドリーなdAppを構築することが可能になります。