In our previous article, we completed the exchange’s risk control system. In this article, we will discuss integrating Solana into the exchange wallet. Solana’s account model, log storage, and confirmation mechanism differ significantly from Ethereum-based chains. Simply applying Ethereum routines can easily lead to pitfalls. Here, we outline the overall approach to recording Solana.
Understanding the Unique Aspects of Solana
Solana Account Model
Solana uses a program and data separation model. Programs are shareable, while program data is stored separately in PDA (Program Derived Address) accounts. Since programs are shared, Token Mint accounts are used to distinguish different tokens. Token Mint accounts store global token metadata such as mint authority, total supply, and decimals.
Each token has a unique Mint account address as an identifier. For example, USD Coin (USDC) on the Solana mainnet has the Mint address EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.
Solana has two sets of Token programs: SPL Token and SPL Token-2022. Each SPL Token has an associated ATA (Associated Token Account) to hold user balances. Token transfers involve calling their respective programs to transfer tokens between ATAs.
Log Limitations on Solana
On Ethereum, token transfers are tracked by parsing historical transfer logs. Solana’s execution logs are not permanently retained by default; they are not part of the ledger state and lack log Bloom filters. Logs may also be truncated during execution.
Therefore, we cannot rely on log scanning for deposit reconciliation. Instead, we must use getBlock or getSignaturesForAddress to parse instructions.
Confirmation and Reorganization in Solana
Solana produces blocks approximately every 400ms. After about 12 seconds (32 confirmations), a block reaches ‘finalized’ status. For low-latency requirements, trusting only finalized blocks is sufficient.
For higher real-time needs, consider potential chain reorganizations, which are rare but possible. Unlike Ethereum, Solana’s consensus does not rely on parentBlockHash to form a chain structure, so fork detection cannot be based solely on parentBlockHash and blockHash mismatches.
To detect reorgs locally, record the blockhash for each slot. If the blockhash for the same slot changes, it indicates a rollback.
Understanding these differences, we can proceed with implementation. First, consider the necessary database modifications:
Database Table Design
Since Solana has two token types, we add a token_type field to the tokens table to distinguish between SPL Token and SPL Token-2022.
Although Solana addresses differ from Ethereum, they can be derived using BIP32/BIP44 with different derivation paths. The existing wallets table can be reused. To support ATA address mapping and chain scanning, add the following tables:
Table Name
Key Fields
Description
solana_slots
slot, block_hash, status, parent_slot
Stores redundant slot info for fork detection and rollback triggers
solana_transactions
tx_hash, slot, to_addr, token_mint, amount, type
Records deposit/withdrawal transactions; tx_hash is unique for tracking signatures
Maps user ATA addresses; enables internal account lookup during scanning
Details:
solana_slots records confirmed/finalized/skipped slots; scanner uses status to decide whether to store or rollback.
solana_transactions stores amounts in lamports or token units, with a ‘type’ field for deposit/withdrawal, and sensitive operations require risk control signatures.
solana_token_accounts links wallet_id and wallet_address with token_mint and ata_address, ensuring ATA uniqueness (wallet_address + token_mint). This is core for scanning logic.
Refer to db_gateway/database.md for detailed table definitions.
Processing User Deposits
Deposit processing involves continuously scanning on-chain data, typically via two methods:
Signature scanning: getSignaturesForAddress
Block scanning: getBlock
Method 1: Signature scanning involves calling getSignaturesForAddress with the user’s ATA address or program ID, passing parameters like before, until, limit. This retrieves signatures related to the address, which can then be fetched via getTransaction for detailed info. Suitable for small account sets.
Method 2: Block scanning involves fetching the latest slot, then calling getBlock for each slot to get transaction details, signatures, and account info. This is more suitable for large account sets.
Note: Due to high transaction throughput and TPS on Solana, parsing and filtering may lag behind block production. Using message queues (Kafka/RabbitMQ) to push potential deposit events for downstream processing can improve efficiency. Hot data can be cached in Redis to prevent queue buildup. For many addresses, sharding by ATA addresses and multiple consumers can enhance throughput.
Alternatively, third-party RPC providers offer indexer services with webhook and account monitoring, which can handle large-scale data parsing.
Block Scanning Workflow
We use method two, with core code in scan/solana-scan modules (blockScanner.ts and txParser.ts). The main steps:
Initial sync / historical data fill (performInitialSync):
Start from the last scanned slot, scan up to the latest slot.
Check every 100 slots for new slots, dynamically updating target.
Use ‘confirmed’ commitment for a balance between speed and certainty.
Continuous scanning (scanNewSlots):
Keep polling for new slots.
Re-validate recent confirmed slots to detect reorgs.
Block parsing (txParser.parseBlock):
Call getBlock with commitment ‘confirmed’ and encoding ‘jsonParsed’.
Iterate through each transaction’s instructions and inner instructions.
Only process successful transactions (tx.meta.err === null).
Instruction parsing (txParser.parseInstruction):
SOL transfers: Match System Program transfer instructions, check if destination address is monitored.
SPL Token transfers: Match Token Program or Token-2022 Program transfer/transferChecked instructions; if destination ATA matches, map to wallet address and token mint via database.
Reorg handling:
Continuously fetch finalizedSlot.
If slot ≤ finalizedSlot, mark as finalized.
For confirmed slots, check if blockhash has changed to detect reorgs.
Wallet Module: walletBusinessService.ts:405-754
Signer Module: solanaSigner.ts:29-122
Test Scripts: requestWithdrawOnSolana.ts
Optimization notes:
Pre-check ATA existence before withdrawal; create ATA if missing (additional fee).
Use computeUnitPrice to prioritize transactions during network congestion.
Summary
Integrating Solana into the exchange does not fundamentally change the architecture but requires adapting to its unique account model, transaction structure, and consensus confirmation mechanism.
Pre-establish and maintain ATA-to-wallet mappings for token transfers, monitor blockhash changes to detect reorgs, and dynamically update transaction statuses from confirmed to finalized.
During withdrawals, fetch the latest blockhash, distinguish token types, and construct appropriate transactions accordingly.
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
Exchange wallet system development — integrating Solana blockchain
In our previous article, we completed the exchange’s risk control system. In this article, we will discuss integrating Solana into the exchange wallet. Solana’s account model, log storage, and confirmation mechanism differ significantly from Ethereum-based chains. Simply applying Ethereum routines can easily lead to pitfalls. Here, we outline the overall approach to recording Solana.
Understanding the Unique Aspects of Solana
Solana Account Model
Solana uses a program and data separation model. Programs are shareable, while program data is stored separately in PDA (Program Derived Address) accounts. Since programs are shared, Token Mint accounts are used to distinguish different tokens. Token Mint accounts store global token metadata such as mint authority, total supply, and decimals.
Each token has a unique Mint account address as an identifier. For example, USD Coin (USDC) on the Solana mainnet has the Mint address EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.
Solana has two sets of Token programs: SPL Token and SPL Token-2022. Each SPL Token has an associated ATA (Associated Token Account) to hold user balances. Token transfers involve calling their respective programs to transfer tokens between ATAs.
Log Limitations on Solana
On Ethereum, token transfers are tracked by parsing historical transfer logs. Solana’s execution logs are not permanently retained by default; they are not part of the ledger state and lack log Bloom filters. Logs may also be truncated during execution.
Therefore, we cannot rely on log scanning for deposit reconciliation. Instead, we must use getBlock or getSignaturesForAddress to parse instructions.
Confirmation and Reorganization in Solana
Solana produces blocks approximately every 400ms. After about 12 seconds (32 confirmations), a block reaches ‘finalized’ status. For low-latency requirements, trusting only finalized blocks is sufficient.
For higher real-time needs, consider potential chain reorganizations, which are rare but possible. Unlike Ethereum, Solana’s consensus does not rely on parentBlockHash to form a chain structure, so fork detection cannot be based solely on parentBlockHash and blockHash mismatches.
To detect reorgs locally, record the blockhash for each slot. If the blockhash for the same slot changes, it indicates a rollback.
Understanding these differences, we can proceed with implementation. First, consider the necessary database modifications:
Database Table Design
Since Solana has two token types, we add a token_type field to the tokens table to distinguish between SPL Token and SPL Token-2022.
Although Solana addresses differ from Ethereum, they can be derived using BIP32/BIP44 with different derivation paths. The existing wallets table can be reused. To support ATA address mapping and chain scanning, add the following tables:
Details:
Refer to db_gateway/database.md for detailed table definitions.
Processing User Deposits
Deposit processing involves continuously scanning on-chain data, typically via two methods:
Method 1: Signature scanning involves calling getSignaturesForAddress with the user’s ATA address or program ID, passing parameters like before, until, limit. This retrieves signatures related to the address, which can then be fetched via getTransaction for detailed info. Suitable for small account sets.
Method 2: Block scanning involves fetching the latest slot, then calling getBlock for each slot to get transaction details, signatures, and account info. This is more suitable for large account sets.
Note: Due to high transaction throughput and TPS on Solana, parsing and filtering may lag behind block production. Using message queues (Kafka/RabbitMQ) to push potential deposit events for downstream processing can improve efficiency. Hot data can be cached in Redis to prevent queue buildup. For many addresses, sharding by ATA addresses and multiple consumers can enhance throughput.
Alternatively, third-party RPC providers offer indexer services with webhook and account monitoring, which can handle large-scale data parsing.
Block Scanning Workflow
We use method two, with core code in scan/solana-scan modules (blockScanner.ts and txParser.ts). The main steps:
Reorg handling:
Sample core code snippets:
// blockScanner.ts - scan a single slot async 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 - parse transfer instructions for (const tx of block.transactions) { if (tx.meta?.err) continue; // skip failed tx const instructions = [ …tx.transaction.message.instructions, …(tx.meta.innerInstructions ?? []).flatMap(i => i.instructions), ]; for (const ix of instructions) { // SOL transfer if (ix.programId === SYSTEM_PROGRAM_ID && ix.parsed?.type === ‘transfer’) { if (monitoredAddresses.has(ix.parsed.info.destination)) { // process deposit } } // SPL Token transfer 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)) { // process deposit } } } } }
Deposit Processing Summary:
Withdrawal Process
Solana withdrawals are similar to EVM chains but differ in transaction construction:
Withdrawal workflow:
Sample code for signing:
// SOL transfer instruction const instruction = getTransferSolInstruction({ source: hotWalletSigner, destination: solanaAddress.to, amount: BigInt(amount), }); // Token transfer instruction const instruction = getTransferInstruction({ source: sourceAta, destination: destAta, authority: hotWalletSigner, amount: BigInt(amount), });
// Build transaction message const transactionMessage = pipe( createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayerSigner(hotWalletSigner, tx), tx => setTransactionMessageLifetime({ blockhash, lastValidBlockHeight }), tx => appendTransactionMessageInstruction(instruction), );
// Sign transaction const signedTx = await signTransactionMessageWithSigners(transactionMessage); // Encode for sending const signedTransaction = getBase64EncodedWireTransaction(signedTx);
Wallet Module: walletBusinessService.ts:405-754
Signer Module: solanaSigner.ts:29-122
Test Scripts: requestWithdrawOnSolana.ts
Optimization notes:
Summary
Integrating Solana into the exchange does not fundamentally change the architecture but requires adapting to its unique account model, transaction structure, and consensus confirmation mechanism.
Pre-establish and maintain ATA-to-wallet mappings for token transfers, monitor blockhash changes to detect reorgs, and dynamically update transaction statuses from confirmed to finalized.
During withdrawals, fetch the latest blockhash, distinguish token types, and construct appropriate transactions accordingly.