Gateアプリをダウンロードするにはスキャンしてください
qrCode
その他のダウンロードオプション
今日はこれ以上表示しない

取引所ウォレットシステム開発——Solanaチェーンの接続

前回は取引所のリスク管理体系を補完しましたが、今回は取引所のウォレットをSolanaチェーンに接続します。Solanaのアカウントモデル、ログ保存、確認メカニズムはEthereum系のブロックチェーンと大きく異なるため、Ethereumのやり方をそのまま適用すると落とし穴にはまりやすいです。以下にSolanaの全体的な思考を整理します。

Solanaの特徴を理解する

Solanaのアカウントモデル

Solanaはプログラムとデータを分離したモデルを採用しており、プログラムは共有可能です。一方、プログラムのデータはPDA(Program Derived Address)アカウントに個別に保存されます。プログラムは共用されるため、Token Mintを用いて異なるTokenを区別します。Token Mintアカウントには、ミント権限(mint_authority)総供給量(supply)、**小数点以下の桁数(decimals)**などのグローバルメタデータが格納されます。
各Tokenには一意のMintアドレスがあり、例えばUSDC(USD Coin)のSolanaメインネット上のMintアドレスはEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vです。

Solana上には二つのTokenプログラムがあります。一つはSPL Token、もう一つはSPL Token-2022です。各SPL Tokenは独立したATA(Associated Token Account)を持ち、ユーザーの残高を管理します。Tokenの送金時には、それぞれのプログラムを呼び出し、TokenがATAアカウント間で移動します。

Solanaのログ制限

Ethereumでは履歴の送金ログを解析してTokenの送金を把握しますが、Solanaの実行ログはデフォルトでは永続的に保存されません。Solanaのログは帳簿の状態(state)に属さず(ログのブルームフィルターもありません)、実行中に出力が切り捨てられることもあります。
そのため、「ログをスキャンしてチャージを照合する」方法は使えず、代わりにgetBlockやgetSignaturesForAddressを用いて命令を解析します。

Solanaの確認とリオーガナイゼーション

Solanaのブロック生成時間は約400msです。32確認(約12秒)を経るとfinalized状態になります。リアルタイム性がそれほど必要でなければ、単にfinalizedブロックだけを信頼すれば良いです。
より高いリアルタイム性を求める場合、ブロックのリオーガナイゼーション(再編)を考慮する必要があります。SolanaのコンセンサスはparentBlockHashに依存せず、EthereumのようにparentBlockHashと異なるblockHashを比較してフォークを判断できません。
では、どうやってブロックのリオーガナイゼーションを判断するか?
ローカルでブロックをスキャンする際には、slotのblockhashを記録します。同じslotでblockhashが変わった場合はリオーガナイゼーションが発生したと判断します。

Solanaの違いを理解したら、次は実装に取り掛かります。まずはデータベースの修正内容を見てみましょう。

データベース設計

Solanaには二種類のTokenがあるため、tokensテーブルにはtoken_typeカラムを追加し、spl-tokenとspl-token-2022を区別します。

また、SolanaのアドレスはEthereumと異なりますが、BIP32やBIP44の派生は可能です。ただし、派生パスは異なるため、既存のwalletsテーブルをそのまま使います。
ただし、ATAアドレスのマッピングやSolanaのブロックスキャンをサポートするために、以下の三つのテーブルを追加します。

テーブル名 主要フィールド 説明
solana_slots slot, block_hash, status, parent_slot slot情報の冗長保存。フォーク検知とリロールのトリガーに使用
solana_transactions tx_hash, slot, to_addr, token_mint, amount, type チャージ・出金などの取引詳細。tx_hashはユニーク。ダブルサイン追跡用
solana_token_accounts wallet_id, wallet_address, token_mint, ata_address ユーザーのATAマッピングを記録。スキャンモジュールでATAアドレスから内部アカウントを逆引き
  • solana_slotsはconfirmed/finalized/skippedを記録し、スキャナーは状態に応じてDB登録やリロールを行います。
  • solana_transactionsはlamportsまたは最小単位で登録し、typeフィールドでdeposit/withdrawなどを区別。リスク管理の署名も必要です。
  • solana_token_accountsはwallets/usersと外部キー関係を持ち、ATAの一意性(wallet_address + token_mint)を保証します。これがスキャンロジックのコアインデックスです。

詳細なテーブル定義はdb_gateway/database.mdを参照してください。

ユーザーのチャージ処理

チャージ処理は、Solanaチェーン上のデータを継続的にスキャンする必要があります。一般的には二つの方法があります。

  1. サインネーションスキャン:getSignaturesForAddressを呼び出し、対象アドレスの署名を取得(例:ATAアドレスやprogramID)
  2. ブロックスキャン:getBlockを呼び出し、最新のslotの取引詳細や署名、アカウント情報を取得し、必要なデータを抽出

方法1:アドレスの署名をスキャンします。getSignaturesForAddress(address, { before, until, limit })を呼び出し、ユーザー生成のATAアドレスやprogramIDを指定します。これにより、増分の署名を取得し、getTransaction(signature)で詳細情報を得ます。この方法は少数のアカウントや少量のデータに適しています。
アカウント数が非常に多い場合は、ブロックスキャンの方が効率的です。

方法2:ブロックスキャン。最新のslotを取得し、getBlock(slot)で取引詳細や署名、アカウント情報を取得します。指令やアカウントをフィルタリングして必要なデータを抽出します。

補足:Solanaの取引はTPSが高いため、実運用では解析やフィルタリングが追いつかない場合があります。その場合はメッセージキュー(KafkaやRabbitMQ)を使い、トークン送金の潜在的チャージイベントを抽出してキューにプッシュし、後続のコンシューマーが正確にDBに書き込みます。
また、ホットデータはRedisに保存し、キューの詰まりを防ぎます。ユーザーアドレスが多い場合はATAアドレスごとにシャーディングし、複数のコンシューマーで並行処理します。

自己スキャンを避けたい場合、サードパーティのRPCサービス(例:Indexerサービス)を利用する手もあります。Webhookやアカウント監視、高度なフィルタリングを提供し、大量データの解析負荷を肩代わりします。

ブロックスキャンの流れ

私たちは方法2を採用し、コードはscan/solana-scanモジュールのblockScanner.tstxParser.tsに実装しています。主な流れは以下の通りです。

1. 初期同期・履歴ブロックの補完(performInitialSync)

  • 前回スキャンしたslotから最新まで逐次スキャン
  • 100slotごとに新slotの発生を確認し、動的にターゲットを更新
  • confirmedコミットメントを用いてブロックを取得

2. スキャンフェーズ(scanNewSlots)

  • 新slotの発生を継続的に監視
  • 最近のconfirmed slotを再検証(リロール検知)

3. ブロック解析(txParser.parseBlock)

  • getBlock(slot, { commitment: "confirmed", encoding: "jsonParsed" })を呼び出し
  • 各取引のtransaction.message.instructionsmeta.innerInstructionsを走査
  • 成功した取引のみ処理(tx.meta.err === null

4. 指令解析(txParser.parseInstruction)

  • SOL送金:System Programのtransferタイプを検出し、宛先アドレスが監視リストにあるかを確認
  • Token送金:Token ProgramまたはToken-2022 ProgramのtransfertransferCheckedを検出し、宛先ATAアドレスを取得。データベースのマッピングからウォレットアドレスやTokenMintに変換。

リロールの具体的処理
finalizedSlotを継続的に取得し、slot <= finalizedSlotならfinalizedとマーク。confirmed状態のブロックについては、blockhashの変更を確認してリロールを検知します。

以下はコアコード例です。

// blockScanner.ts - 単一スロットのスキャン
async function scanSingleSlot(slot: number) {
  const block = await solanaClient.getBlock(slot);
  if (!block) {
    await insertSlot({ slot, status: 'skipped' });
    return;
  }
  const finalizedSlot = await getCachedFinalizedSlot();
  const status = slot <= finalizedSlot ? 'finalized' : 'confirmed';
  await processBlock(slot, block, status);
}

// txParser.ts - 送金指令の解析
for (const tx of block.transactions) {
  if (tx.meta?.err) continue; // 失敗した取引はスキップ
  const instructions = [
    ...tx.transaction.message.instructions,
    ...(tx.meta.innerInstructions ?? []).flatMap(i => i.instructions)
  ];
  for (const ix of instructions) {
    // SOL送金
    if (ix.programId === SYSTEM_PROGRAM_ID && ix.parsed?.type === 'transfer') {
      if (monitoredAddresses.has(ix.parsed.info.destination)) {
        // 監視リストにある宛先
      }
    }
    // Token送金
    if (ix.programId === TOKEN_PROGRAM_ID || ix.programId === TOKEN_2022_PROGRAM_ID) {
      if (ix.parsed?.type === 'transfer' || ix.parsed?.type === 'transferChecked') {
        const ataAddress = ix.parsed.info.destination;
        const walletAddress = ataToWalletMap.get(ataAddress);
        if (walletAddress && monitoredAddresses.has(walletAddress)) {
          // 監視対象のウォレット
        }
      }
    }
  }
}

チャージ取引を検知したら、DB Gatewayとリスク管理のダブル署名を用いて安全に資金流動表に記録します。

出金

Solanaの出金フローはEVM系と類似していますが、構造に違いがあります。

  1. Tokenには普通のSPL-TokenとSPL-Token-2022の二種類があり、それぞれのprogramIDが異なるため、トランザクション構築時に区別します(現状、Token-2022は少ないため未対応も選択肢)。
  2. Solanaの取引は二つの部分から成ります:署名群(signatures)とメッセージ(message)。
    messageはヘッダー、アカウントキー、recentBlockhash、instructionsを含み、ハッシュ化と署名が行われます。
    recentBlockhashは150ブロック(約1分)の有効期限があり、取引ごとに最新のものを取得して使用します。
    出金トランザクションは手動審査が必要な場合、最新のrecentBlockhashを取得し、再署名を行います。

出金の流れ

![出金フロー図]

実装例:署名モジュールのコアコード

// SOL送金指令
const instruction = getTransferSolInstruction({
  source: hotWalletSigner,
  destination: solanaAddress.to,
  amount: BigInt(amount)
});

// Token送金指令
const instruction = getTransferInstruction({
  source: sourceAta,
  destination: destAta,
  authority: hotWalletSigner,
  amount: BigInt(amount)
});

// 取引メッセージの構築
const transactionMessage = pipe(
  createTransactionMessage({ version: 0 }),
  tx => setTransactionMessageFeePayerSigner(hotWalletSigner, tx),
  tx => setTransactionMessageLifetimeUsingBlockhash({ blockhash, lastValidBlockHeight }),
  tx => appendTransactionMessageInstruction(instruction)
);

// 署名
const signedTx = await signTransactionMessageWithSigners(transactionMessage);

// 完成した取引のエンコード
const signedTransaction = getBase64EncodedWireTransaction(signedTx);

ウォレットモジュールからネットワークへ送信

const solanaRpc = chainConfigManager.getSolanaRpc();
const txSignature = await solanaRpc.sendTransaction(signedTransaction, ...);

完全な出金実装コードは以下にあります。

  • Walletモジュール:walletBusinessService.ts(405-754行目)
  • Signerモジュール:solanaSigner.ts(29-122行目)
  • テストスクリプト:requestWithdrawOnSolana.ts

ここでの最適化ポイントは二つです。

  1. ATA事前チェック:出金前にターゲットアドレスのATAアカウントが存在するか確認し、なければ作成コストを考慮して事前に作成
  2. 優先度設定:ネットワークが混雑している場合、computeUnitPriceを調整して取引の優先度を高める

まとめ

取引所がSolanaチェーンを導入する際の全体アーキテクチャは大きく変わりませんが、Solanaの独特なアカウントモデル、取引構造、コンセンサス確認メカニズムに適応する必要があります。
チャージ処理では、事前にATAとウォレットアドレスのマッピングを構築・維持し、Token送金の識別に利用します。ブロックハッシュの変化を監視してブロックのリオーガナイゼーションを検知し、取引状態を動的に更新します(confirmed→finalized)。
出金時には、getLatestBlockhashを用いて取引パラメータを取得し、Solana、SPL Token、Token-2022を区別して異なる取引を構築します。

SOL0.51%
ETH1.27%
USDC-0.02%
原文表示
このページには第三者のコンテンツが含まれている場合があり、情報提供のみを目的としております(表明・保証をするものではありません)。Gateによる見解の支持や、金融・専門的な助言とみなされるべきものではありません。詳細については免責事項をご覧ください。
  • 報酬
  • コメント
  • リポスト
  • 共有
コメント
0/400
コメントなし
  • ピン