🦋

【Web3 / Blockchain/React/TypeScript】Solanaでのトランザクション処理をコードから学ぶ

2025/01/30に公開

はじめに

仮想通貨やブロックチェーン上での取引は、実際にどのようにプログラムコードで行われているのでしょうか?
この記事では Solana を例に、Web3 ならではのネットワーク構造・RPC ノード・トランザクションの仕組みを、シンプルな入金(Deposit)コードをとおして解説します。


1. Web3 / Blockchain とは?

1-1. 従来のWeb2との違い

  • Web2: 中央サーバー or クラウド (例: AWS, GCP) がメイン。
  • Web3: 分散型ネットワークを使い、誰でもトランザクション(取引)を検証できる。

Solanaのようなブロックチェーンでは、各ノードがトランザクションを検証してブロックに取り込み、一貫した状態を保ちます。


2. RPC ノードとネットワーク (Devnet / Mainnet / Testnet)

2-1. RPCノードとは

  • ブロックチェーンとのやりとりをする「ゲートウェイサーバー」的存在。
  • Solana では、Connection(RPC_URL) オブジェクトを介してブロックチェーンにアクセス。
  • 公式RPC (https://api.mainnet-beta.solana.com) だけでなく、Helius や QuickNode など、外部のインフラプロバイダが提供するRPCノードもある。

2-2. Devnet / Mainnet / Testnet の違い

  • Mainnet: 本番環境。実際の SOL やトークンに価値がある。
  • Devnet: 開発用ネットワーク。試験用に使われる。
  • Testnet: 主にアップグレードや新機能テストに使われる公式ネットワーク。

多くの開発者は Mainnet の貴重な資産をリスクにさらさないため、Devnet で最初に機能を試し、問題がなければ Mainnet にデプロイします。


3. コード全体像 – Solana の Depositトランザクション

以下は React + TypeScript で書かれたサンプルコードです。
Wallet(Solflare) と接続して、ユーザーが入力した金額(USDC)をVaultアドレスへ送金(Deposit)する流れを示します。

import React, { useState, useEffect, useRef } from 'react';
import Solflare from '@solflare-wallet/sdk';
import {
  Connection,
  PublicKey,
  SendTransactionError,
  Transaction,
} from '@solana/web3.js';
import { useNavigate } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { Line } from 'react-chartjs-2';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';
import {
  getAssociatedTokenAddress,
  createAssociatedTokenAccountInstruction,
  createTransferInstruction,
  TOKEN_PROGRAM_ID,
} from '@solana/spl-token';

// ChartJS init
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);

// Env variables
const IS_PRODUCTION = process.env.REACT_APP_ENV === 'production';
const RPC_URL = IS_PRODUCTION
  ? process.env.REACT_APP_RPC_URL_PROD!
  : process.env.REACT_APP_RPC_URL_DEV!;
const USDC_MINT_ADDRESS = IS_PRODUCTION
  ? process.env.REACT_APP_USDC_MINT_PROD!
  : process.env.REACT_APP_USDC_MINT_DEV!;
const VAULT_PUBLIC_KEY = IS_PRODUCTION
  ? process.env.REACT_APP_VAULT_ADDRESS_PROD!
  : process.env.REACT_APP_VAULT_ADDRESS_DEV!;

// PostDepositModal ...
interface PostDepositModalProps {
  depositSentAmount: number;
  preDepositBalance: number;
  postBalance: number;
  onClose: () => void;
  onAutoDisconnect: () => void;
}
const PostDepositModal: React.FC<PostDepositModalProps> = (...) => {
  // 省略
};

const SuperchargerVault: React.FC = () => {
  // React state ...
  const walletRef = useRef<Solflare>(new Solflare());
  const connection = useRef<Connection>(new Connection(RPC_URL)).current;

  // ...
  
  const handleDeposit = async () => {
    console.log('[handleDeposit] Deposit button clicked!');
    if (isDepositing) return; // 連打防止
    if (depositInProgressOtherTab) {
      // 他タブ実行中のフラグ
      return;
    }
    setIsDepositing(true);
    localStorage.setItem('SUPERCHARGER_DEPOSIT_IN_PROGRESS', 'true');

    try {
      // 金額チェック
      const val = parseFloat(depositAmount);
      if (val > (usdcBalance || 0)) {
        // 残高超過エラー
      }
      
      // Transaction構築
      const wallet = walletRef.current;
      if (!wallet.publicKey) throw new Error('No wallet');
      
      const { blockhash } = await connection.getLatestBlockhash('finalized');
      const depositLamports = Math.round(val * 1_000_000);
      const usdcMint = new PublicKey(USDC_MINT_ADDRESS);
      const vaultPubkey = new PublicKey(VAULT_PUBLIC_KEY);
      const userTokenAccount = await getAssociatedTokenAddress(usdcMint, wallet.publicKey);
      const vaultTokenAccount = await getAssociatedTokenAddress(usdcMint, vaultPubkey);
      
      const transferIx = createTransferInstruction(
        userTokenAccount,
        vaultTokenAccount,
        wallet.publicKey,
        depositLamports,
        [],
        TOKEN_PROGRAM_ID
      );
      const transaction = new Transaction().add(transferIx);
      transaction.feePayer = wallet.publicKey;
      transaction.recentBlockhash = blockhash;

      // 署名
      const signedTx = await wallet.signTransaction(transaction);
      // 送信
      const signature = await connection.sendRawTransaction(signedTx.serialize(), {
        skipPreflight: false,
        preflightCommitment: 'processed',
      });
      console.log('[handleDeposit] signature:', signature);

      // 確認
      const confirm = await connection.confirmTransaction(signature, 'finalized');
      if (confirm.value.err) {
        throw new Error('トランザクションの確認に失敗しました');
      }
      // 成功処理 ...
      
    } catch (error) {
      // エラー処理 ...
    } finally {
      setIsDepositing(false);
      localStorage.setItem('SUPERCHARGER_DEPOSIT_IN_PROGRESS', 'false');
    }
  };

  // ReactのJSXレンダリング ...
  return (
    <div>
      {/* Vault UI / Wallet Connect UI */}
    </div>
  );
};

export default SuperchargerVault;

</details>


4. Deposit処理の流れを図解 (mermaid)

Solana の トランザクション送信ウォレットの署名 の一連のやりとりを、簡単なシーケンス図で示します。

このように、ウォレット拡張が秘密鍵で署名 し、アプリは署名済みトランザクションをネットワークに送る、という流れがポイントです。


5. Signature(署名)の仕組み

  • Solana上のトランザクションは、秘密鍵 で署名されることで「このユーザーが正当に送金を発行した」ことを証明します。
  • 公開鍵(=アドレス) に対応する秘密鍵で署名すると、ブロックチェーン上のバリデータは署名の正当性を検証し、本物の所有者が送金要求した と判断します。
  • コードでは、ウォレット拡張(Solflare) などが代わりに署名を行い、その結果(署名済みトランザクション)を受け取って connection.sendRawTransaction() で送信します。

6. Devnet / Testnet / Mainnet の使い分け

  1. Mainnet

    • 本番。SOL やトークンに実際の価値がある。
    • 間違いが許されにくい。
  2. Devnet

    • 開発用ネットワーク。トークンはテスト用で、本物の価値はない
    • requestAirdrop() などで自由にSOLを入手し、トランザクションの挙動を試せる。
  3. Testnet

    • Mainnet のアップグレード前テストなどに使われることが多い。
    • Devnet に比べると、さらに公式的・安定的に試験されるが、一般ユーザはあまり使わない場合も。

このサンプルコードでは .envREACT_APP_ENV=development を設定し、RPC_URL も Devnet 用に切り替えると、Devnet で同様のフローを実行できます。


7. コードのポイント

  1. 多重送信の防止
    • isDepositing ステートと、localStorage フラグ SUPERCHARGER_DEPOSIT_IN_PROGRESS を併用して、同タブでの連打別タブとの競合を防ぐ。
  2. Legacy Transaction
    • Solana にはVersioned Transaction等ありますが、このサンプルは Legacy Transaction でシンプルに書いています。
  3. Token Transfer
    • createTransferInstruction(userTokenAccount, vaultTokenAccount, ...) で USDC を送金。
    • Solanaの SPL Token プログラム (TOKEN_PROGRAM_ID) を指定してトランスファーを行う。

8. 安全性について

  1. ウォレット拡張(Phantom, Solflareなど) が秘密鍵を管理
    • アプリ本体には秘密鍵を渡さないため、ユーザー側がセキュリティを確保できる。
  2. Transaction Signature
    • ブロックチェーン上のノードは、公開鍵とトランザクションの署名を突き合わせて正当性を検証。
  3. Devnetで試験
    • Mainnet での大きな資産移動をいきなり行うのではなく、Devnet でテスト → 問題なければ Mainnet に移行するのが一般的。

9. まとめ

  • このコードからわかるように、ウォレット署名RPCノードに送信ブロックチェーンで検証 という三段階で Deposit が完了します。
  • Devnet / Mainnet / Testnet は同じコードでもRPCエンドポイントMintアドレスを変えるだけで接続先を切り替えられます。
  • Signature(署名) は秘密鍵を使った暗号技術で、ユーザー本人の取引を証明する重要な仕組みです。

これが Web3 / Blockchain 流の「トランザクションをコードで見る」アプローチの一例でした。

これから Web3 アプリケーション開発を始める方は、まずはDevnet でトランザクションを送るコードを動かして、ウォレット署名RPCノード の動きを体感してみるのをおすすめします!

Discussion