前回は取引所のリスク管理体系を補完しましたが、今回は取引所のウォレットを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のブロックスキャンをサポートするために、以下の三つのテーブルを追加します。
詳細なテーブル定義はdb_gateway/database.mdを参照してください。
ユーザーのチャージ処理
チャージ処理は、Solanaチェーン上のデータを継続的にスキャンする必要があります。一般的には二つの方法があります。
方法1:アドレスの署名をスキャンします。getSignaturesForAddress(address, { before, until, limit })を呼び出し、ユーザー生成のATAアドレスやprogramIDを指定します。これにより、増分の署名を取得し、getTransaction(signature)で詳細情報を得ます。この方法は少数のアカウントや少量のデータに適しています。 アカウント数が非常に多い場合は、ブロックスキャンの方が効率的です。
getSignaturesForAddress(address, { before, until, limit })
getTransaction(signature)
方法2:ブロックスキャン。最新のslotを取得し、getBlock(slot)で取引詳細や署名、アカウント情報を取得します。指令やアカウントをフィルタリングして必要なデータを抽出します。
getBlock(slot)
補足:Solanaの取引はTPSが高いため、実運用では解析やフィルタリングが追いつかない場合があります。その場合はメッセージキュー(KafkaやRabbitMQ)を使い、トークン送金の潜在的チャージイベントを抽出してキューにプッシュし、後続のコンシューマーが正確にDBに書き込みます。 また、ホットデータはRedisに保存し、キューの詰まりを防ぎます。ユーザーアドレスが多い場合はATAアドレスごとにシャーディングし、複数のコンシューマーで並行処理します。
自己スキャンを避けたい場合、サードパーティのRPCサービス(例:Indexerサービス)を利用する手もあります。Webhookやアカウント監視、高度なフィルタリングを提供し、大量データの解析負荷を肩代わりします。
ブロックスキャンの流れ
私たちは方法2を採用し、コードはscan/solana-scanモジュールのblockScanner.tsとtxParser.tsに実装しています。主な流れは以下の通りです。
scan/solana-scan
blockScanner.ts
txParser.ts
1. 初期同期・履歴ブロックの補完(performInitialSync)
2. スキャンフェーズ(scanNewSlots)
3. ブロック解析(txParser.parseBlock)
getBlock(slot, { commitment: "confirmed", encoding: "jsonParsed" })
transaction.message.instructions
meta.innerInstructions
tx.meta.err === null
4. 指令解析(txParser.parseInstruction)
transfer
transferChecked
リロールの具体的処理: finalizedSlotを継続的に取得し、slot <= finalizedSlotならfinalizedとマーク。confirmed状態のブロックについては、blockhashの変更を確認してリロールを検知します。
finalizedSlot
slot <= finalizedSlot
以下はコアコード例です。
// 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系と類似していますが、構造に違いがあります。
message
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, ...);
完全な出金実装コードは以下にあります。
walletBusinessService.ts
solanaSigner.ts
requestWithdrawOnSolana.ts
ここでの最適化ポイントは二つです。
computeUnitPrice
まとめ
取引所がSolanaチェーンを導入する際の全体アーキテクチャは大きく変わりませんが、Solanaの独特なアカウントモデル、取引構造、コンセンサス確認メカニズムに適応する必要があります。 チャージ処理では、事前にATAとウォレットアドレスのマッピングを構築・維持し、Token送金の識別に利用します。ブロックハッシュの変化を監視してブロックのリオーガナイゼーションを検知し、取引状態を動的に更新します(confirmed→finalized)。 出金時には、getLatestBlockhashを用いて取引パラメータを取得し、Solana、SPL Token、Token-2022を区別して異なる取引を構築します。
getLatestBlockhash
27.67K 人気度
65.9K 人気度
262.21K 人気度
15.83K 人気度
8.05K 人気度
取引所ウォレットシステム開発——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のブロックスキャンをサポートするために、以下の三つのテーブルを追加します。
詳細なテーブル定義はdb_gateway/database.mdを参照してください。
ユーザーのチャージ処理
チャージ処理は、Solanaチェーン上のデータを継続的にスキャンする必要があります。一般的には二つの方法があります。
方法1:アドレスの署名をスキャンします。
getSignaturesForAddress(address, { before, until, limit })を呼び出し、ユーザー生成のATAアドレスやprogramIDを指定します。これにより、増分の署名を取得し、getTransaction(signature)で詳細情報を得ます。この方法は少数のアカウントや少量のデータに適しています。アカウント数が非常に多い場合は、ブロックスキャンの方が効率的です。
方法2:ブロックスキャン。最新のslotを取得し、
getBlock(slot)で取引詳細や署名、アカウント情報を取得します。指令やアカウントをフィルタリングして必要なデータを抽出します。自己スキャンを避けたい場合、サードパーティのRPCサービス(例:Indexerサービス)を利用する手もあります。Webhookやアカウント監視、高度なフィルタリングを提供し、大量データの解析負荷を肩代わりします。
ブロックスキャンの流れ
私たちは方法2を採用し、コードは
scan/solana-scanモジュールのblockScanner.tsとtxParser.tsに実装しています。主な流れは以下の通りです。1. 初期同期・履歴ブロックの補完(performInitialSync)
2. スキャンフェーズ(scanNewSlots)
3. ブロック解析(txParser.parseBlock)
getBlock(slot, { commitment: "confirmed", encoding: "jsonParsed" })を呼び出しtransaction.message.instructionsとmeta.innerInstructionsを走査tx.meta.err === null)4. 指令解析(txParser.parseInstruction)
transferタイプを検出し、宛先アドレスが監視リストにあるかを確認transferやtransferCheckedを検出し、宛先ATAアドレスを取得。データベースのマッピングからウォレットアドレスやTokenMintに変換。リロールの具体的処理:
finalizedSlotを継続的に取得し、slot <= finalizedSlotならfinalizedとマーク。confirmed状態のブロックについては、blockhashの変更を確認してリロールを検知します。以下はコアコード例です。
チャージ取引を検知したら、DB Gatewayとリスク管理のダブル署名を用いて安全に資金流動表に記録します。
出金
Solanaの出金フローはEVM系と類似していますが、構造に違いがあります。
messageはヘッダー、アカウントキー、recentBlockhash、instructionsを含み、ハッシュ化と署名が行われます。recentBlockhashは150ブロック(約1分)の有効期限があり、取引ごとに最新のものを取得して使用します。出金トランザクションは手動審査が必要な場合、最新の
recentBlockhashを取得し、再署名を行います。出金の流れ
![出金フロー図]
実装例:署名モジュールのコアコード
ウォレットモジュールからネットワークへ送信
完全な出金実装コードは以下にあります。
walletBusinessService.ts(405-754行目)solanaSigner.ts(29-122行目)requestWithdrawOnSolana.tsここでの最適化ポイントは二つです。
computeUnitPriceを調整して取引の優先度を高めるまとめ
取引所がSolanaチェーンを導入する際の全体アーキテクチャは大きく変わりませんが、Solanaの独特なアカウントモデル、取引構造、コンセンサス確認メカニズムに適応する必要があります。
チャージ処理では、事前にATAとウォレットアドレスのマッピングを構築・維持し、Token送金の識別に利用します。ブロックハッシュの変化を監視してブロックのリオーガナイゼーションを検知し、取引状態を動的に更新します(confirmed→finalized)。
出金時には、
getLatestBlockhashを用いて取引パラメータを取得し、Solana、SPL Token、Token-2022を区別して異なる取引を構築します。