👻

【Web3/ブロックチェーン】Web3.jsを用いたSolana Vaultの開発体験記:DepositとTransactionの実装方法

2025/01/28に公開

はじめに

こんにちは!今回は、Web3.jsを使用してSolanaブロックチェーン上にVaultを構築し、デポジットとトランザクション機能を実装するまでの開発経験を共有したいと思います。このプロジェクトを通じて学んだことや直面した課題、解決策について詳しく解説します。
なお、今回はある程度BlockChainに対する用語を知っていることを前提で執筆しております。
ご了承ください。

目的と背景

ブロックチェーン技術の進化に伴い、分散型アプリケーション(DApps)の開発が急速に進んでいます。特に、Solanaは高速かつ低コストなトランザクション処理が可能なことで注目を集めています。本記事では、Solana上にVaultを構築し、ユーザーがUSDCをデポジットおよびWithdrawできる機能を実装する過程を紹介します。


基本概念の解説

Web3.jsとは

Web3.jsは、Ethereumブロックチェーンと対話するためのJavaScriptライブラリです。これを使用することで、スマートコントラクトのデプロイやトランザクションの送信、ウォレットの管理などが可能になります。Solanaの場合、@solana/web3.jsという専用のライブラリが存在し、同様の機能を提供します。

Solanaについて

Solanaは、高速でスケーラブルなブロックチェーンプラットフォームです。1秒間に数千から数万件のトランザクションを処理できる能力を持ち、低い手数料でのトランザクションが特徴です。これにより、DAppsの開発やデプロイが効率的に行えます。

デポジットとトランザクションの仕組み

デポジットは、ユーザーが自分のウォレットからVaultに資金を預け入れるプロセスです。一方、**Withdraw(引き出し)**は、Vaultからユーザーのウォレットに資金を戻すプロセスです。これらの操作はブロックチェーン上でトランザクションとして処理され、安全かつ透明に行われます。


開発環境のセットアップ

必要なツールとライブラリ

  • Node.js: JavaScriptのランタイム環境
  • npmまたはyarn: パッケージマネージャー
  • React: フロントエンドライブラリ
  • TypeScript: 型安全なJavaScript
  • Web3.jsライブラリ: @solana/web3.js, @solflare-wallet/sdk, @solana/spl-token
  • その他: react-router-dom, framer-motion, chart.js, react-chartjs-2

プロジェクトの初期設定

以下のコマンドを使用してReactプロジェクトを作成し、必要な依存関係をインストールします。

npx create-react-app solana-vault --template typescript
cd solana-vault
npm install @solana/web3.js @solflare-wallet/sdk @solana/spl-token react-router-dom framer-motion chart.js react-chartjs-2

ウォレット接続の実装

Solflare、Phantom、MetaMaskとの接続方法

ユーザーがウォレットを通じてVaultと対話できるようにするために、Solflare、Phantom、MetaMaskといったウォレットとの接続を実装します。各ウォレットには特徴があり、ユーザーに選択の自由を提供することが重要です。
この手順につきましては、以前の僕の記事で詳しくまとめているのでそちらを参照してください。


USDC残高の取得と表示

USDCトークンアカウントの取得方法

USDCはSolana上でSPLトークンとして発行されています。ユーザーのウォレットに関連付けられたUSDCアカウントを取得するために、getAssociatedTokenAddress関数を使用します。

const fetchUsdcBalance = async (publicKey: PublicKey) => {
  try {
    const usdcMint = new PublicKey(USDC_MINT_ADDRESS);
    const usdcTokenAddress = await getAssociatedTokenAddress(usdcMint, publicKey);

    // ATA(Associated Token Account)の存在確認
    const accountInfo = await connection.getAccountInfo(usdcTokenAddress);

    if (!accountInfo) {
      console.log('USDCトークンアカウントが存在しません。新規作成が必要です。');
      setUsdcBalance('0.00'); // 存在しない場合は残高を0として扱う
      
      // ATAを作成するトランザクションを構築
      const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');

      const transaction = new Transaction({
        recentBlockhash: blockhash,
        feePayer: publicKey,
      }).add(
        createAssociatedTokenAccountInstruction(
          publicKey,          // payer
          usdcTokenAddress,   // ATAのアドレス
          publicKey,          // オーナー
          usdcMint            // ミント
        )
      );

      // トランザクションをウォレットで署名
      const wallet = walletRef.current;
      const signedTransaction = await wallet.signTransaction(transaction);

      // 署名済みトランザクションをシリアライズ
      const serializedTransaction = signedTransaction.serialize();

      // トランザクションを送信
      const signature = await connection.sendRawTransaction(serializedTransaction, {
        skipPreflight: false,
        preflightCommitment: 'processed',
      });
      console.log('ATA作成のトランザクション署名:', signature);

      // トランザクションの確認
      const confirmation = await connection.confirmTransaction(
        {
          signature,
          blockhash,
          lastValidBlockHeight,
        },
        'finalized'
      );

      if (confirmation.value.err) {
        console.error('トランザクション確認中にエラーが発生しました:', confirmation.value.err);
        throw new Error('ATA作成失敗');
      }

      console.log('ATAが作成されました');
    }

    // ATAが存在する場合、残高を取得
    const tokenAccountBalance = await connection.getTokenAccountBalance(usdcTokenAddress);
    const balance = tokenAccountBalance?.value?.uiAmount || 0;

    console.log('取得したUSDC残高:', balance);
    setUsdcBalance(balance.toFixed(6));
  } catch (error) {
    console.error('USDC残高取得エラー:', error);
    setUsdcBalance('N/A'); // エラー時にはN/Aを設定
  }
};

エラーハンドリング

トークンアカウントが存在しない場合、新規に作成する処理を実装します。これにより、ユーザーが初めてVaultを利用する際にもスムーズに操作が行えます。

図解

USDC残高取得のプロセスをMermaidで以下のように表現します。


デポジット機能の実装

ユーザー入力の処理

ユーザーがデポジットする金額を入力するフォームを作成し、入力値のバリデーションを行います。

<input
  type="number"
  className="flex-1 bg-[#1A1A1A] rounded px-4 py-3 text-white"
  placeholder="0.00"
  value={depositAmount}
  onChange={(e) => setDepositAmount(e.target.value)}
/>

トランザクションの作成と送信

デポジット機能では、ユーザーのUSDCトークンアカウントからVaultのトークンアカウントへ資金を転送するトランザクションを作成し、送信します。

const handleDeposit = async () => {
  if (isDepositing) return; // 二重送信を防止

  try {
    setIsDepositing(true);
    setDepositStatus(null); // ステータスをリセット

    const wallet = walletRef.current;
    if (!wallet || !wallet.publicKey) {
      throw new Error('ウォレットが接続されていません');
    }

    // 入力金額を整数に丸めて BigInt に変換
    const depositAmountNumber = parseFloat(depositAmount);
    if (isNaN(depositAmountNumber) || depositAmountNumber <= 0) {
      throw new Error('有効な金額を入力してください');
    }
    const depositAmountLamports = BigInt(Math.round(depositAmountNumber * 1_000_000)); // USDCは小数点以下6桁

    const userPublicKey = wallet.publicKey;
    const usdcMint = new PublicKey(USDC_MINT_ADDRESS);
    const vaultPublicKey = new PublicKey(VAULT_PUBLIC_KEY);

    // トークンアカウントのアドレスを取得
    const userTokenAccount = await getAssociatedTokenAddress(usdcMint, userPublicKey);
    const vaultTokenAccount = await getAssociatedTokenAddress(usdcMint, vaultPublicKey);

    // 最新のブロックハッシュを取得
    const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');

    // トランザクションを作成
    const transaction = new Transaction({
      recentBlockhash: blockhash,
      feePayer: userPublicKey,
    });

    // トランスファーインストラクションを作成
    const transferInstruction = createTransferInstruction(
      userTokenAccount, // 送信元
      vaultTokenAccount, // 送信先
      userPublicKey, // 送信者
      depositAmountLamports, // 送金量 (BigInt)
      [],
      TOKEN_PROGRAM_ID
    );

    // トランザクションにインストラクションを追加
    transaction.add(transferInstruction);

    // トランザクションをウォレットで署名
    const signedTransaction = await wallet.signTransaction(transaction);

    // トランザクションを送信
    const signature = await connection.sendRawTransaction(signedTransaction.serialize(), {
      skipPreflight: false,
      preflightCommitment: 'processed',
    });

    console.log('トランザクション送信成功:', signature);

    // トランザクション確認
    const confirmation = await connection.confirmTransaction(
      {
        signature,
        blockhash,
        lastValidBlockHeight,
      },
      'finalized'
    );

    if (confirmation.value.err) {
      console.error('トランザクション確認エラー:', confirmation.value.err);
      throw new Error('トランザクションの確認に失敗しました');
    }

    console.log('Depositが成功しました:', signature);

    // トランザクションログの取得
    const logs = await connection.getParsedTransaction(signature, {
      commitment: 'confirmed',
    });
    console.log('トランザクションログ:', logs);

    setDepositStatus('Depositが成功しました');
    setDepositAmount(''); // 入力フィールドをリセット
    await fetchUsdcBalance(userPublicKey); // 残高更新
  } catch (error: any) {
    console.error('トランザクションエラー:', error);

    // 送金が成功している可能性がある場合のエラーメッセージ
    if (error.message.includes('already been processed')) {
      setDepositStatus(
        'トランザクションは既に処理されています。残高が減っているか確認してください。'
      );
    } else {
      setDepositStatus(`Depositに失敗しました: ${error.message}`);
    }
  } finally {
    setIsDepositing(false); // 処理が完了したらローディング状態を解除
  }
};

図解

デポジットトランザクションの流れをMermaidで以下のように表現します。


Withdraw機能の実装

Withdrawロジックの説明

Withdraw機能では、VaultからユーザーのウォレットへUSDCを引き出すプロセスを実装します。デポジットと同様に、トランザクションを作成し、ウォレットで署名・送信します。

コード例

以下は、Withdraw機能を実装するためのコードスニペットです。

// Withdraw Amount State
const [withdrawAmount, setWithdrawAmount] = useState<string>('');
const [withdrawStatus, setWithdrawStatus] = useState<string | null>(null);
const [isWithdrawing, setIsWithdrawing] = useState(false); // ローディング状態

// Withdrawハンドラ
const handleWithdraw = async () => {
  if (isWithdrawing) return; // 二重送信を防止

  try {
    setIsWithdrawing(true);
    setWithdrawStatus(null); // ステータスをリセット

    const wallet = walletRef.current;
    if (!wallet || !wallet.publicKey) {
      throw new Error('ウォレットが接続されていません');
    }

    const withdrawAmountNumber = parseFloat(withdrawAmount);
    if (isNaN(withdrawAmountNumber) || withdrawAmountNumber <= 0) {
      throw new Error('有効な金額を入力してください');
    }
    const withdrawAmountLamports = BigInt(Math.round(withdrawAmountNumber * 1_000_000)); // USDCは小数点以下6桁

    const userPublicKey = wallet.publicKey;
    const usdcMint = new PublicKey(USDC_MINT_ADDRESS);
    const vaultPublicKey = new PublicKey(VAULT_PUBLIC_KEY);

    // トークンアカウントのアドレスを取得
    const userTokenAccount = await getAssociatedTokenAddress(usdcMint, userPublicKey);
    const vaultTokenAccount = await getAssociatedTokenAddress(usdcMint, vaultPublicKey);

    // 最新のブロックハッシュを取得
    const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');

    // トランザクションを作成
    const transaction = new Transaction({
      recentBlockhash: blockhash,
      feePayer: userPublicKey,
    });

    // トランスファーインストラクションを作成
    const transferInstruction = createTransferInstruction(
      vaultTokenAccount, // 送信元
      userTokenAccount, // 送信先
      userPublicKey, // 送信者
      withdrawAmountLamports, // 送金量 (BigInt)
      [],
      TOKEN_PROGRAM_ID
    );

    // トランザクションにインストラクションを追加
    transaction.add(transferInstruction);

    // トランザクションをウォレットで署名
    const signedTransaction = await wallet.signTransaction(transaction);

    // トランザクションを送信
    const signature = await connection.sendRawTransaction(signedTransaction.serialize(), {
      skipPreflight: false,
      preflightCommitment: 'processed',
    });

    console.log('トランザクション送信成功:', signature);

    // トランザクション確認
    const confirmation = await connection.confirmTransaction(
      {
        signature,
        blockhash,
        lastValidBlockHeight,
      },
      'finalized'
    );

    if (confirmation.value.err) {
      console.error('トランザクション確認エラー:', confirmation.value.err);
      throw new Error('トランザクションの確認に失敗しました');
    }

    console.log('Withdrawが成功しました:', signature);

    // トランザクションログの取得
    const logs = await connection.getParsedTransaction(signature, {
      commitment: 'confirmed',
    });
    console.log('トランザクションログ:', logs);

    setWithdrawStatus('Withdrawが成功しました');
    setWithdrawAmount(''); // 入力フィールドをリセット
    await fetchUsdcBalance(userPublicKey); // 残高更新
  } catch (error: any) {
    console.error('トランザクションエラー:', error);

    // 送金が成功している可能性がある場合のエラーメッセージ
    if (error.message.includes('already been processed')) {
      setWithdrawStatus(
        'トランザクションは既に処理されています。残高が増えているか確認してください。'
      );
    } else {
      setWithdrawStatus(`Withdrawに失敗しました: ${error.message}`);
    }
  } finally {
    setIsWithdrawing(false); // 処理が完了したらローディング状態を解除
  }
};

図解

Withdrawトランザクションの流れをMermaidで以下のように表現します。


エラーハンドリングとユーザー通知

エラーメッセージの表示方法

ユーザーが操作中にエラーが発生した場合、わかりやすいメッセージを表示することが重要です。例えば、トランザクションが既に処理されている場合や、ウォレットが接続されていない場合など、具体的な原因を伝えます。

{depositStatus && (
  <p
    className={`mt-2 text-sm ${
      depositStatus.includes('成功') ? 'text-green-500' : 'text-red-500'
    }`}
  >
    {depositStatus}
  </p>
)}

成功通知の実装

トランザクションが成功した場合、ユーザーに成功メッセージを表示します。これにより、ユーザーは操作が正常に完了したことを確認できます。

{withdrawStatus && (
  <p
    className={`mt-2 text-sm ${
      withdrawStatus.includes('成功') ? 'text-green-500' : 'text-red-500'
    }`}
  >
    {withdrawStatus}
  </p>
)}

セキュリティ考慮事項

ウォレット情報の保護

ユーザーのプライベートキーやシードフレーズは絶対にフロントエンド側で保存しないように注意します。ウォレット接続時には、ウォレット自身が管理するセキュリティ機能を利用し、アプリケーションは公開鍵のみを扱うようにします。

スマートコントラクトのセキュリティ

Vaultに関連するスマートコントラクト(この場合、Solana上のプログラム)のセキュリティも重要です。以下のベストプラクティスを遵守します。

  • コードレビューと監査: 信頼できる第三者によるコードレビューを実施する。
  • 最小権限の原則: 必要最小限の権限しか持たせない。
  • テストの徹底: ユニットテストや統合テストを充実させる。

まとめと今後の展望

プロジェクトの振り返り

本記事では、Web3.jsを用いてSolanaブロックチェーン上にVaultを構築し、デポジットとWithdraw機能を実装するまでの過程を詳しく解説しました。ウォレット接続からトランザクションの送信、エラーハンドリングまで、実際のコード例を通じて具体的に学ぶことができました。
会社の都合でソースコードやデプロイしたLinkなどは共有できません。ご了承くださいませ。

実際のアプリの画像

今後の改善点や追加機能

  • 他のトークンのサポート: USDC以外のSPLトークンにも対応する機能を追加。
  • UI/UXの向上: より直感的で使いやすいインターフェースの設計。
  • リアルタイムデータの取得: トランザクションの進行状況や最新のブロック情報をリアルタイムで表示。
  • 通知機能: トランザクションの完了時にユーザーにプッシュ通知を送信。

参考資料


まとめ

本記事では、Web3.jsを用いてSolanaブロックチェーン上にVaultを構築し、デポジットとWithdraw機能を実装する方法について詳しく解説しました。具体的なコード例やMermaidを用いた図解を通じて、Web3開発の基礎から応用までを学ぶことができました。開発経験を記事としてまとめることで、他の開発者との知識共有やコミュニティへの貢献が可能です。ぜひ、皆さんも自身のプロジェクトを進めながら、学んだことをアウトプットしてみてください。


Happy Coding!

Discussion