In the previous article, we completed the exchange's risk control system. This article will cover integrating exchange wallets into the Solana chain. Solana's account model, log storage, and confirmation mechanism differ significantly from Ethereum-based chains. Following Ethereum's approach can easily lead to pitfalls. Below, we'll outline the overall approach to Solana.
Understanding the Unique Solana
Solana Account Model
Solana uses a model that separates programs and data. Programs can be shared, while program data is stored separately through PDA (Program Derived Address) accounts. Because programs are shared, Token Mint is needed to distinguish different tokens.
The Token Mint account stores global metadata for the token, such as minting authority, total supply, and decimal places. Each token has a unique Mint account address as an identifier. For example, the Mint address for USD Coin (USDC) on the Solana mainnet is EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. Solana uses two token programs: SPL Token and SPL Token-2022. Each SPL Token has its own independent ATA (Associated Token Account) to store the user's balance. Token transfers actually involve calling their respective programs to move tokens between ATA accounts. Solana Log Limitations: On Ethereum, token transfers are obtained by parsing historical transfer logs. However, Solana's execution logs are not permanently retained by default. Solana's logs are not part of the ledger state (and lack a log Bloom filter), and output may be truncated during execution. Therefore, we cannot perform deposit reconciliation by "scanning logs," but must use `getBlock` or `getSignaturesForAddress` to parse the instructions. Solana Confirmation and Reorganization: Solana's block time is 400ms, and it reaches finalized after 32 confirmations (approximately 12 seconds). If real-time requirements are not high, a simple method is to only trust finalized blocks. If higher real-time performance is desired, the possibility of block reorganization needs to be considered, although it is rare. However, Solana consensus does not rely on parentBlockHash to form a chain structure, and cannot determine forks by the difference between parentBlockHash and blockHash in the database, as is the case with Ethereum. So, what method should be used to determine if a block has been reorganized? When scanning blocks locally, we need to record the blockhash of the slot. If there is a change in the blockhash of the same slot, it means that a rollback has occurred. Understanding the differences in Solana allows us to begin implementation. Let's first examine the database modifications needed: Database Table Design: Since Solana has two types of tokens, we need to add a `token_type` field to the `tokens` table to distinguish between `spl-token` and `spl-token-2022`. Although Solana addresses differ from Ethereum addresses, they can still be derived using BIP32 and BIP44, albeit through different paths. Therefore, we only need to use the existing `wallets` table. However, to support ATA address mapping and Solana block scanning, the following three tables need to be added: style="text-align: left;">
Table Name
Key Fields
Description
solana_slots
slot
,block_hash,status,parent_slot
Redundant slot information to facilitate fork detection and trigger rollback
solana_transactions
tx_hash
,slot,to_addr,token_mint,amount,type
Stores transaction details such as deposits/withdrawals,tx_hash&nsp;unique, used for dual-signature tracking
Records user ATA mappings. The scan module can reverse lookup of internal accounts by ata_address
Where:
solana_slots will record confirmed/finalized/skipped. The scanner decides whether to write to the database or roll back based on the status. ...>
solana_slots will record confirmed/finalized/skipped.
Where:
solana_slots will record confirmed/finalized/skipped.
Where:
solana_transactions is stored in the database using either 'lamports' or the smallest unit of token, with the 'type' field to distinguish business scenarios such as deposit/withdraw. Sensitive writes still require risk control signatures.
solana_token_accounts establishes a foreign key relationship with 'wallets'/'users' to ensure the uniqueness of ATAs ('wallet_address + token_mint' unique), which is also the core index of the scanning logic.
For detailed table definitions, please refer to db_gateway/database.md
Processing User Top-ups
Processing user top-ups requires continuously scanning data on the Solana chain. There are generally two methods:
Scan Signatures: getSignaturesForAddress()
Scan Blocks: getBlock()
Method 1: Scan the address signature by calling `getSignaturesForAddress(address, { before, until, limit })`, passing the address we are interested in as a parameter. This address is the ATA address we generated for the user, or it can be the programID (note the transfer of spl-token). The instruction call (which does not include the mint address) continuously retrieves incremental signatures by controlling the before and until parameters, and then obtains the transaction information data through `getTransaction(signature)`. This method is suitable for situations with a small amount of data or a small number of accounts. If the number of accounts is very large, block scanning is more suitable; we use the block scanning method here. Method 2: The block scanning method continuously obtains the latest Slot, calls `getBlock(slot)` to obtain complete transaction details, signatures, or accounts, and then filters out the data we need based on the instruction and account. Note: Due to the large transaction volume and high TPS of Solana, in a production environment, the parsing and filtering speed may not keep up with Solana's block generation speed. In this case, a message queue is needed to simply filter all token transfers and push possible "potential deposit events" to a message queue such as Kafka/RabbitMQ. Subsequent queue consumer modules then accurately filter and write the data to the database. To speed up filtering, some hot data needs to be stored in Redis to avoid queue backlog. If there are many user addresses, sharding by ATA address can be used, with multiple consumers listening to different shards to improve efficiency. Alternatively, if you don't want to scan blocks yourself, you can use a third-party RPC service provider to offer additional indexer services, such as webhooks, account monitoring, and advanced filtering support, which can handle the pressure of parsing large amounts of data.
Block Scanning Process
We used Method 2. The relevant code is in blockScanner.ts and txParser.ts under the scan/solana-scan module. The main process is as follows:
SOL transfer: Matches the `transfer` type of `System Program (11111...)`.
SOL transfer: Matches the `transfer` type of `System Program (11111...)`.
SOL transfer: Matches the `transfer` type of `System Program (11111...)`.
destination` to see if it is in the monitored address list. SPL Token Transfer: Matches `Token Program` or `Token-2022 Program` with `transfer/transferChecked`, and `destination` matches the ATA address. Then, it is mapped to the wallet address and TokenMint address via the database.
Rollback Specific Processing: The program will continuously retrieve `finalizedSlot`. When slot ≤ `finalizedSlot`, it is marked as `finalized`. For blocks that are still in the `confirmed` state, the program checks whether the blockhash has changed to determine whether to rollback.
The sample core code is as follows:
// 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 transactions 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)) { // ... } } // 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; // ATA address const walletAddress = ataToWalletMap.get(ataAddress); // Map to wallet address if (walletAddress && monitoredAddresses.has(walletAddress)) { // ... After scanning a deposit transaction, the security of dual signature using DB Gateway + risk control is maintained. After verification, the data is written to the fund flow table's credits. Afterward, Solana's withdrawal process is similar to that of the EVM chain, but there are differences in transaction construction: On Solana, there are two types of tokens: ordinary SPL-Token and SPL-Token 2022. The program IDs of the two tokens are different, and they need to be distinguished when constructing transaction instructions. (Currently, SPL-Token 2022 is relatively rare; you can also choose not to support token 2022.) Solana transactions consist of two parts: `signatures` (a set of ed25519 signatures) and a message (containing header, accountKeys, recentBlockhash, and instructions). The message content is hashed and signed, and placed in `signatures`. Solana transactions do not use nonces; instead, `recentBlockhash` is used to constrain the transaction's validity period. `recentBlockhash` has a validity period of only 150 blocks (approximately 1 minute). Therefore, each time a transaction is initiated, `recentBlockhash` needs to be obtained from the blockchain in real time to be updated. If a withdrawal transaction requires manual review, then `recentBlockhash` must be obtained again to meet the transaction structure and request a signature again.
Withdrawal process
Actually, this is where you get the transaction Blockhash
The complete withdrawal implementation code is located at:
Wallet module: walletBusinessService.ts:405-754
Signer module: solanaSigner.ts:29-122
Test script: requestWithdrawOnSolana.ts
Preview
Gain a broader understanding of the crypto industry through informative reports, and engage in in-depth discussions with other like-minded authors and readers. You are welcome to join us in our growing Coinlive community:https://t.me/CoinliveSG