掃描下載 Gate App
qrCode
更多下載方式
今天不再提醒

交易所錢包系統開發——接入 Solana 鏈

上一篇我們補齊了交易所的風控體系,這一篇將介紹如何接入 Solana 鏈的錢包。Solana 的帳戶模型、日誌存儲和確認機制與以太坊系鏈有很大不同,如果沿用以太坊的套路,容易踩坑。以下我們梳理一下記錄 Solana 的整體思路。

了解獨特的 Solana

Solana 帳戶模型

Solana 採用程序與數據分離的模型,程序是可以共用的,而程序的數據則通過 PDA(Program Derived Address)帳戶單獨保存。由於程序是共用的,因此需要 Token Mint 來區分不同的 Token。Token Mint 帳戶存儲代幣的全局元數據,例如 鑄造權限(mint_authority)總供應量(supply)小數位數(decimals) 等。
每個代幣都有唯一的 Mint 帳戶地址作為標識符,例如 USD Coin(USDC)在 Solana 主網的 Mint 地址是 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v。

在 Solana 上有兩套 Token 程序,一是 SPL Token,另一是 SPL Token-2022。每種 SPL Token 都有獨立的 ATA(Associated Token Account)來保存用戶的餘額。在 Token 轉賬時,實際上是調用各自的程序在 Token 在 ATA 帳戶之間轉移。

Solana 日誌限制

在以太坊上,是通過解析歷史的轉賬日誌來獲取 Token 轉賬信息,但 Solana 的執行日誌預設不會永久保留,且不屬於帳本狀態(也沒有日誌的布隆過濾器),在執行過程中可能會被截斷。
因此,我們不能通過“掃描日誌”來做充值對帳,而是要使用 getBlock 或 getSignaturesForAddress 來解析指令。

Solana 確認與重組

Solana 出塊時間約為 400ms,經過 32 個確認(約 12 秒)即可達到 finalized 狀態。如果對實時性要求不高,可以只信任 finalized 的區塊。
若需更高的實時性,則需考慮可能出現的區塊重組,雖然較少發生。但由於 Solana 共識不依賴 parentBlockHash 形成鏈結構,不能像以太坊那樣通過 parentBlockHash 和資料庫中的 blockHash 不一致來判斷分叉。那應該用什麼方法來判斷區塊被重組呢?
在本地掃塊時,我們需要記錄 slot 的 blockhash,如果同一 slot 的 blockhash 發生變化,就表示發生了回滾。

理解 Solana 的不同,接下來就可以著手實現了,先看看資料庫需要做哪些修改:

資料庫表設計

由於 Solana 有兩種類型的 Token,因此,我們需要在 tokens 表中添加一個 token_type,用來區分 spl-token 和 spl-token-2022。

儘管 Solana 地址與以太坊不同,但同樣可以通過 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 映射,scan 模塊可按 ata_address 反查內部帳戶

其中:

  • solana_slots 會記錄 confirmed/finalized/skipped,掃描器根據狀態決定是否入庫或回滾。
  • solana_transactions 以 lamports 或 token 最小單位入庫,並帶有 type 字段區分 deposit/withdraw 等業務場景,敏感操作仍需風控簽名。
  • solana_token_accounts 與 wallets/users 建立外鍵關係,保證 ATA 的唯一性(wallet_address + token_mint 唯一),也是掃描邏輯的核心索引。

詳細表定義可參考 db_gateway/database.md

處理用戶充值

處理用戶充值,需要不斷掃描 Solana 鏈上數據,通常有兩種方法:

  1. 掃簽名:getSignaturesForAddress
  2. 掃塊:getBlock

方法一:掃描地址的簽名,調用 getSignaturesForAddress(address, { before, until, limit }),傳入我們關注的地址(即為用戶生成的 ATA 地址,也可以是 programID)。注意 spl-token 的轉賬指令調用不包含 mint 地址,通過控制 before、until 參數不斷拉取增量簽名,再用 getTransaction 獲取交易信息。

此方法適合數據量或帳號較少的情況,若帳戶數非常大,則使用掃塊更合適,我們這裡採用掃塊方法。

方法二:掃描最新的 Slot,調用 getBlock(slot),獲取完整交易詳情、簽名或帳戶,然後根據指令與帳戶篩選出所需數據。

備註:由於 Solana 交易流量大、TPS 高,在生產環境中,解析速度可能跟不上出塊速度,這時需要使用消息隊列,將所有 token 轉賬過濾出來,推送到 Kafka/RabbitMQ 等消息隊列,再由後端消費模塊精準過濾並寫入資料庫。為加快過濾效率,一些熱點數據可存 Redis,避免隊列堆積。如果用戶地址數量很多,可以按 ATA 地址分片,讓多個消費者監聽不同分片。

如果不想自己掃塊,還可以使用第三方 RPC 服務商提供的 Indexer 服務,例如 Webhook、帳戶監聽和高階過濾支持,能承擔大數據解析壓力。

掃塊流程

我們採用方法二,相關代碼位於 scan/solana-scan 模塊下的 blockScanner.ts 和 txParser.ts,主要流程如下:

1. 初始同步階段、補充歷史區塊(performInitialSync)

  • 從上次掃描的 slot 開始,逐個掃描到最新的 slot
  • 每 100 個 slot 檢查是否有新 slot 產生,動態更新目標
  • 使用 confirmed 承諾獲取區塊,兼顧實時性

2. 掃描階段(scanNewSlots)

  • 不斷檢查是否有新 slot 產生
  • 重新驗證最近的 confirmed slot(檢測回滾)

3. 區塊解析(txParser.parseBlock)

  • 調用 getBlock(slot, { commitment: “confirmed”, encoding: “jsonParsed” })
  • 遍歷每筆交易的 transaction.message.instructions 和 meta.innerInstructions
  • 只處理成功的交易(tx.meta.err === null)

4. 指令解析(txParser.parseInstruction)

  • SOL 轉賬:匹配 System Program 的 transfer 類型,地址直接匹配 destination 是否在監控列表中
  • SPL Token 轉賬:匹配 Token Program 或 Token-2022 Program 的 transfer/transferChecked,destination 匹配 ATA 地址,然後通過資料庫映射到錢包地址和 TokenMint 地址。

回滾處理
程序會不斷獲取 finalizedSlot,若 slot ≤ finalizedSlot 即標記為 finalized。對於仍在 confirmed 狀態的區塊,通過比對 blockhash 是否變更來判斷是否回滾。

示例核心代碼如下:

// blockScanner.ts - 掃描單個槽位
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 - 解析轉賬指令
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; // ATA 地址
const walletAddress = ataToWalletMap.get(ataAddress); // 映射到錢包地址
if (walletAddress && monitoredAddresses.has(walletAddress)) {
// …
}
}
}
}
在掃描到充值交易後,依循 DB Gateway + 風控雙簽的安全措施,驗證後將數據寫入資金流水表 credits。

提現

Solana 的提現流程與 EVM 鏈類似,但在交易構建上有所不同:

  1. 在 Solana 上,有兩種 Token:普通 SPL-Token 和 SPL-Token 2022,兩者的 programID 不同,構造交易指令時需區分(目前 SPL-Token 2022 比較少,也可選擇不支持)。
  2. Solana 的交易由兩部分組成:signatures(簽名集,使用 ed25519)和 message(包含 header、accountKeys、recentBlockhash、instructions)。message 內容經哈希後簽名,存放在 signatures 中。Solana 交易沒有 nonce,而是用 recentBlockhash 來約束交易有效期,約 150 個區塊(約 1 分鐘)有效。每次發起交易時,需從鏈上獲取最新的 recentBlockhash,若提現需人工審核,則需重新獲取 recentBlockhash,並用於簽名。

提現流程

![提現流程圖]

實際上,將獲取交易的 Blockhash 放在風控檢查之後會更合理。

Signer 模塊簽名交易核心代碼如下:

根據交易類型構建不同指令:

// 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)
});

構建並簽名交易消息:

// 使用 @solana/kit 構建交易
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(hotWalletSigner, tx),
tx => setTransactionMessageLifetime({ blockhash, lastValidBlockHeight }),
tx => appendTransactionMessageInstruction(instruction)
);
// 簽名交易
const signedTx = await signTransactionMessageWithSigners(transactionMessage);
// 返回兩種編碼:
// 1. Base64 編碼的完整交易(用於發送到網絡)
const signedTransaction = getBase64EncodedWireTransaction(signedTx);

錢包模塊將交易發送到網絡

// 使用 @solana/web3.js 發送交易
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 帳戶已創建,否則需額外費用創建 ATA。
  2. 優先費用:在網絡擁堵時,可設置 computeUnitPrice 提高交易優先級。

總結

接入 Solana 鏈在整體架構上沒有變化,關鍵在於適配其獨特的帳戶模型、交易結構以及共識確認機制。
在處理充值時,預先建立並維護 ATA 與錢包地址的映射表,用於 Token 轉賬識別;統一監控 blockhash 變化以檢測區塊重組,動態更新交易狀態(confirmed → finalized)。
在提現時,使用 getLatestBlockhash 獲取交易參數,同時區分 Sol、SPL Token 和 Token-2022,構造不同的交易。

SOL4.97%
ETH5.46%
USDC-0.03%
查看原文
此頁面可能包含第三方內容,僅供參考(非陳述或保證),不應被視為 Gate 認可其觀點表述,也不得被視為財務或專業建議。詳見聲明
  • 讚賞
  • 留言
  • 轉發
  • 分享
留言
0/400
暫無留言
交易,隨時隨地
qrCode
掃碼下載 Gate App
社群列表
繁體中文
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)