回填是指檢索區塊鏈歷史資料,以填充資料庫、分析過往趨勢或審計交易的過程。無論您是需要從創世區塊開始對整條鏈進行索引,還是僅需追蹤過去 24 小時的活動,高效地檢索歷史資料都是一項關鍵的基礎設施挑戰。
Streams 透過一鍵式範本、伺服器端篩選、批次處理與壓縮選項,以及保證將資料傳送至您指定的目標位置,讓資料回填變得簡單。 您無需編寫複雜的腳本來輪詢 RPC 端點,只需配置一個 Stream、設定區塊範圍,並可選擇套用篩選器以僅擷取所需Streams 資料可靠且大規模地Streams 至您的目標位置(例如:Webhook、S3、PostgreSQL、Azure Storage 等)。
為何要使用Streams 回填?
Streams 基礎架構方面的事務,例如重試、區塊排序及錯誤處理,讓您能專注於應用程式的資料邏輯。
| 特色 | 說明 |
|---|---|
| 篩選器 | 您可以在傳送資料前,透過篩選器在伺服器端對資料進行處理與轉換。請使用 解碼 EVM 收據 用於十六進位解碼、建立自訂有效載荷,以及透過以下方式進行除錯: console.log(). |
| 批次處理與壓縮 | 您可以設定 資料集批次大小 (例如 10-100),以每筆請求批次處理多個區塊。您也可以啟用 壓縮格式:「gzip」 在 目的地設定. |
| 鍵值儲存系統 | 您可以儲存觀察清單, ABIs,或設定值,並透過以下方式在篩選器中存取這些值: qnLib 方法。您也可以透過 REST API 管理數值。 |
| REST API | 您可以Streams REST API 以程式方式建立、更新、暫停及刪除Streams 。 |
| 多鏈 | 每個 Stream 針對一條鏈/網路。您可以Streams 執行多個 Stream,以進行多鏈資料補全。請參閱「支援的鏈」。 |
| 即時轉換 | 您可以省略 end_range 在回填完成後繼續串流。您可以使用 elastic_batch_enabled 在處理端自動縮小批次大小,並 與尖端保持距離 以減少 組織重整 頻率。 |
回填成本估算
回填歷史區塊鏈資料會根據處理的區塊數量,並採用網路資料集乘數,來消耗 API 信用點數。Streams 與 RPCStreams 同一個 API 信用點數池。
在開始之前,請使用API 信用額度計算器來估算您的補足成本。該計算器會顯示:
- 填補可用歷史區塊所需的 API 積分總數
- 每個區塊的積分數取決於您所選的資料集和網路
- 哪些網路支援回填功能?(例如,Solana 目前並不支援回填)
如何進行資料回填
-
請選擇您的區塊鏈和網路。請參閱Streams 支援的區塊鏈清單。
-
設定您的區塊範圍。您可以選擇從創世區塊開始,或從特定區塊高度開始。至於結束區塊,您可以設定為特定高度,或選擇連續串流模式以持續接收新區塊。
-
請選擇您的資料集。 Streams 多種資料集,例如區塊、附收據的區塊、交易、日誌等。請選擇符合您使用情境的資料集。有關特定區塊鏈的資料集,請參閱下方的「依生態系統補填資料」。
-
套用篩選條件(可選)。使用伺服器端篩選功能,以縮小您想要接收的資料範圍。
-
選擇您的目的地。設定您希望將資料傳送至何處,例如Webhooks、S3、PostgreSQL 等。請參閱「目的地」文件以獲取更多詳細資訊。
-
請在「串流」設定頁面中檢查連線狀態,並傳送測試資料,以確保所有設定皆正確無誤。
-
開始直播。
請參閱下方的「回填技巧」,了解有關批次處理、壓縮及效能優化的建議。
依生態系統分類的補充資料
Streams 為任何生態系統(包括Ethereum、Bitcoin、Solana 等)補填資料。由於各生態系統的資料結構與格式各不相同,請在下方選擇您的生態系統,以查看該鏈專屬的資料集、範例篩選條件及回應結果。
- Ethereum EVM 鏈
- Solana
- Bitcoin
- XRP Ledger
Ethereum EVM 鏈
Streams 多條 EVM 鏈,包括Ethereum、Base、Arbitrum 及BNB Smart Chain。所有 EVM 鏈皆採用相似的架構與資料結構,因此篩選條件與資料包格式在各鏈上通常相同,但可能存在某些特定於各鏈的差異。
解碼 EVM 資料
在使用 EVM 相容鏈時,您可以透過 解碼 EVM 收據 函式。此實用工具以原始交易收據及您的合約 ABI 作為輸入,將原始十六進位資料轉換為人類可讀的格式。
解碼過程會自動:
- 將交易日誌中的事件簽名與提供的 ABI 進行比對。
- 根據參數的類型(位址、整數、字串等)進行解碼。
- 以帶有名稱參數的結構化資料形式傳回
解碼日誌物件。
進一步了解此功能:解碼 EVM 資料
可用的資料來源
對於Ethereum、Base、Arbitrum 和BNB Smart Chain 等 EVM 鏈,您可以使用以下資料集來進行資料回填。
如需完整的 JSON 規格及各資料集的詳細資訊,請參閱「資料來源」文件。
| 資料來源 | 說明 |
|---|---|
| 區塊 | 由 eth_getBlockByNumber 函式所回傳的區塊物件陣列 |
| 附收據的區塊 | 一個包含複合資料集的物件陣列,其中包含由 `eth_getBlockByNumber` 和 `eth_getBlockReceipts` 函式所傳回的區塊與收據 |
| 交易 | 一組交易物件的陣列,其形式與區塊資料中的「transactions」陣列所呈現的一致 |
| 日誌 | 一組由日誌物件組成的陣列,其形式與交易收據中「logs」陣列內的呈現方式相同 |
| 收據 | 一個陣列的陣列,每個陣列都包含由 eth_getBlockReceipts 返回的收據物件 |
| 追蹤 (debug_trace) | 由 debug_traceBlock 回傳的追蹤資料陣列之陣列 |
| 軌跡 (trace_block) | 由 trace_block 回傳的追蹤資料陣列之陣列 |
| 附收據的區塊 + debug_trace | 一個包含複合資料集的物件陣列,其中包含來自 debug_traceBlock 的區塊、收據和追蹤記錄 |
| 附收據的區塊 + trace_block | 一個包含複合資料集的物件陣列,其中包含來自 trace_block 的區塊、收據和追蹤記錄 |
資料來源的可用性因鏈而異。部分資料集(特別是 Traces)可能並非在所有 EVM 網路中皆受支援。請參閱「資料來源」頁面以查看最新的支援對照表。
範例:ERC-20 代幣轉帳
以下範例函式會篩選一組交易,透過檢查輸入資料中的 轉帳 方法簽名。
- 去
- JavaScript
- 回應
// Chain: Ethereum
// Dataset: Transactions
// Test with block: 23977403
type Payload struct {
Data [][]Tx `json:"data"`
Metadata map[string]interface{} `json:"metadata"`
}
type Tx struct {
Hash string `json:"hash"`
From string `json:"from"`
To string `json:"to"`
Input string `json:"input"`
BlockNumber string `json:"blockNumber"`
}
func Filter(qn *qn.QNContext, payload Payload) interface{} {
// The standard ERC-20 transfer(address,uint256) method signature
const transferMethodID = "0xa9059cbb"
var filteredTransactions []map[string]interface{}
// Loop through all blocks in the batch (data is [][]tx: outer = block, inner = txs)
for _, transactions := range payload.Data {
for _, tx := range transactions {
// Ensure the input carries a full transfer(address,uint256) call:
// "0x" + 4-byte selector + two 32-byte words = 138 hex chars.
if !strings.HasPrefix(tx.Input, transferMethodID) || len(tx.Input) < 138 {
continue
}
// Decode the 'to' address (skip selector + left-padding)
toAddress := "0x" + tx.Input[34:74]
// Decode the 'value' (amount) from the trailing 32-byte word.
// uint256 can exceed int64, so convert hex → decimal without math/big.
amount := hexToDecimal(tx.Input[74:])
filteredTransactions = append(filteredTransactions, map[string]interface{}{
"txHash": tx.Hash,
"fromAddress": tx.From,
"toAddress": toAddress,
"amount": amount,
"tokenContract": tx.To,
"blockNumber": tx.BlockNumber,
})
}
}
// Return nil if no transfers were found — skips delivery to your destination
// (API credits are still consumed based on blocks processed).
if len(filteredTransactions) == 0 {
return nil
}
return map[string]interface{}{"transactions": filteredTransactions}
}
// hexToDecimal converts an arbitrary-length hex string (with or without a "0x"
// prefix) to its decimal string form, safe for full uint256 values.
func hexToDecimal(hexStr string) string {
hexStr = strings.TrimPrefix(strings.ToLower(hexStr), "0x")
hexStr = strings.TrimLeft(hexStr, "0")
if hexStr == "" {
return "0"
}
// digits holds the decimal value little-endian (least-significant first).
digits := []int{0}
for _, c := range hexStr {
v, err := strconv.ParseInt(string(c), 16, 32)
if err != nil {
return "0"
}
carry := int(v)
for i := 0; i < len(digits); i++ {
cur := digits[i]*16 + carry
digits[i] = cur % 10
carry = cur / 10
}
for carry > 0 {
digits = append(digits, carry%10)
carry /= 10
}
}
var sb strings.Builder
for i := len(digits) - 1; i >= 0; i-- {
sb.WriteByte(byte('0' + digits[i]))
}
return sb.String()
}
// Chain: Ethereum
// Dataset: Transactions
// Test with block: 23977403
function main(payload) {
const filteredTransactions = [];
// The standard ERC-20 transfer(address,uint256) method signature
const transferMethodId = "0xa9059cbb";
// Loop through all blocks in the batch
for (const transactions of payload.data) {
for (const transaction of transactions) {
// Ensure the transaction object and input data are valid
if (typeof transaction === 'object' && transaction !== null && typeof transaction.input === 'string') {
// Check if the transaction input starts with the transfer method ID
if (transaction.input.startsWith(transferMethodId)) {
// Decode the 'to' address (skipping method ID and padding)
const toAddress = "0x" + transaction.input.substr(34, 40);
// Decode the 'value' (amount) from the subsequent hex data
const value = BigInt("0x" + transaction.input.substr(74));
filteredTransactions.push({
txHash: transaction.hash,
fromAddress: transaction.from,
toAddress: toAddress,
amount: value.toString(),
tokenContract: transaction.to,
blockNumber: transaction.blockNumber,
});
}
}
}
}
// Return the filtered list, or null if no transfers were found (skips delivery to your destination; API credits are still consumed based on blocks processed)
return filteredTransactions.length > 0 ? { transactions: filteredTransactions } : null;
}
{
"transactions": [
{
"amount": "80007920",
"blockNumber": "0x16dddbb",
"fromAddress": "0xa80f9793051cd1f428ad61b276d431f30dd59b6a",
"toAddress": "0xabd22d07c199a56bafa5e2add3cde1127bc98292",
"tokenContract": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"txHash": "0x013a9cdd1de2ade97a1d79178fe59f2025e41495db050aab0cc706cd2be3b2fe"
},
{
"amount": "71839000",
"blockNumber": "0x16dddbb",
"fromAddress": "0x828ee64b59f33e6c3a6b8d4ad8298aeb65421445",
"toAddress": "0xbb03a0b5159d985c304ab183b80e884ae38c9c89",
"tokenContract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"txHash": "0x2ae18c8918de4744df1d93729e75f89f402723630982224654d18cae047ec74b"
}
]
}
其他資源
- 技術指南:如何補Ethereum 代幣轉帳資料
Solana
Streams 在Solana 上對歷史區間進行回填。由於該網路具有高吞吐量且資料量龐大,回填功能通常用於索引特定時間區間(例如:檢索最近 1 至 2 週的資料),而非從創世區塊開始的整個帳本歷史紀錄。
如需完整的 JSON 規格及各資料集的詳細資訊,請參閱「資料來源」文件。
免費試用帳戶僅能建立即時追蹤區塊鏈末端的Solana Streams 。Solana Streams 的歷史資料補全功能僅限付費方案Streams 。
可用的資料來源
| 資料來源 | 說明 |
|---|---|
| 區塊 | 由 getBlock 函式所回傳的區塊物件陣列 |
| 程式 + 日誌 | 一個包含與程式呼叫相關的日誌訊息及交易元資料的物件陣列 |
範例:追蹤帳戶餘額變動
以下範例函式會篩選涉及特定Solana 的成功交易區塊,並計算 SOL 餘額的變動(delta) 在每筆交易中,針對該帳戶。
- 去
- JavaScript
- 回應
// Chain: Solana
// Dataset: Block
// Test with slot: 282164688
type Payload struct {
Data []SolanaBlock `json:"data"`
Metadata map[string]interface{} `json:"metadata"`
}
type SolanaBlock struct {
ParentSlot int64 `json:"parentSlot"`
BlockTime int64 `json:"blockTime"`
Transactions []SolanaTx `json:"transactions"`
}
// Only the fields the filter reads are declared, so heavy fields such as
// logMessages are never decoded.
type SolanaTx struct {
Meta struct {
Err interface{} `json:"err"`
PreBalances []int64 `json:"preBalances"`
PostBalances []int64 `json:"postBalances"`
} `json:"meta"`
Transaction struct {
Signatures []string `json:"signatures"`
Message struct {
AccountKeys []struct {
Pubkey string `json:"pubkey"`
} `json:"accountKeys"`
} `json:"message"`
} `json:"transaction"`
}
func Filter(qn *qn.QNContext, payload Payload) interface{} {
// Configuration: the target account and filtering preferences.
const (
// The public key of the account to track (e.g. a specific wallet or program)
accountID = "9kwU8PYhsmRfgS3nwnzT3TvnDeuvdbMAXqWsri2X8rAU"
// Set to true to ignore failed transactions (recommended to reduce noise)
skipFailed = true
)
var matchedTransactions []map[string]interface{}
// Loop through all blocks in the batch
for _, block := range payload.Data {
for _, tx := range block.Transactions {
// 1. Skip failed transactions if configured to do so
if skipFailed && tx.Meta.Err != nil {
continue
}
// 2. Find the index of our target account within the account keys
accountIndex := -1
for i, account := range tx.Transaction.Message.AccountKeys {
if account.Pubkey == accountID {
accountIndex = i
break
}
}
// If the target account is not involved in this transaction, skip it
if accountIndex == -1 {
continue
}
// Guard against malformed balance arrays
if accountIndex >= len(tx.Meta.PreBalances) || accountIndex >= len(tx.Meta.PostBalances) {
continue
}
// 3. Retrieve balance information using the account index
preBalance := tx.Meta.PreBalances[accountIndex]
postBalance := tx.Meta.PostBalances[accountIndex]
delta := postBalance - preBalance
// 4. Skip transactions where the account's balance did not change
if delta == 0 {
continue
}
var signature string
if len(tx.Transaction.Signatures) > 0 {
signature = tx.Transaction.Signatures[0]
}
// 5. Construct and append the custom payload
matchedTransactions = append(matchedTransactions, map[string]interface{}{
"signature": signature,
"slot": block.ParentSlot + 1,
"blockTime": block.BlockTime,
"accountKey": accountID,
"preBalance": preBalance,
"postBalance": postBalance,
"delta": delta,
})
}
}
if len(matchedTransactions) == 0 {
return nil // skip delivery
}
return map[string]interface{}{"matchedTransactions": matchedTransactions}
}
// Chain: Solana
// Dataset: Block
// Test with slot: 282164688
// Configuration: Define the target account and filtering preferences
const FILTER_CONFIG = {
// The Public Key of the account to track (e.g., a specific wallet or program)
accountId: '9kwU8PYhsmRfgS3nwnzT3TvnDeuvdbMAXqWsri2X8rAU',
// Set to true to ignore failed transactions (recommended to reduce noise)
skipFailed: true,
};
function main(payload) {
const matchedTransactions = [];
// Loop through all blocks in the batch
for (const block of payload.data) {
for (const tx of block.transactions) {
const result = processTransaction(tx, block);
if (result) {
matchedTransactions.push(result);
}
}
}
return matchedTransactions.length > 0 ? { matchedTransactions } : null;
}
// Helper function to process individual transactions
function processTransaction(transactionWithMeta, block) {
const { meta, transaction } = transactionWithMeta;
// 1. Skip failed transactions if configured to do so
if (FILTER_CONFIG.skipFailed && meta.err !== null) {
return null;
}
// 2. Find the index of our target account within the transaction's account keys
const accountIndex = transaction.message.accountKeys.findIndex(
account => account.pubkey === FILTER_CONFIG.accountId
);
// If the target account is not involved in this transaction, skip it
if (accountIndex === -1) {
return null;
}
// 3. Retrieve balance information using the account index
const preBalance = meta.preBalances[accountIndex];
const postBalance = meta.postBalances[accountIndex];
const delta = postBalance - preBalance;
// 4. Skip transactions where the account's balance did not change
if (delta === 0) {
return null;
}
// 5. Construct and return the custom payload
return {
signature: transaction.signatures[0],
slot: block.parentSlot + 1,
blockTime: block.blockTime,
accountKey: FILTER_CONFIG.accountId,
preBalance,
postBalance,
delta,
};
}
{
"matchedTransactions": [
{
"accountKey": "9kwU8PYhsmRfgS3nwnzT3TvnDeuvdbMAXqWsri2X8rAU",
"blockTime": 1723055487,
"delta": -25000000000000,
"postBalance": 485830395323,
"preBalance": 25485830395323,
"signature": "2nWu9XYxKHWNiwGDHLnHYqrF3uGZCN5subE3rbuFqnxwoGQ11FxSEoz6CffmssYhqC43ewDyhiAhvPZNMSzbqVMC",
"slot": 282164688
}
]
}
其他資源
- 技術指南:如何補全Solana 資料
Bitcoin
Streams Bitcoin Bitcoin 的回填功能。這些區塊鏈採用 UTXO(未花費交易輸出)模型,這與基於帳戶的區塊鏈有所不同。
如需完整的 JSON 規格及各資料集的詳細資訊,請參閱「資料來源」文件。
可用的資料來源
| 資料來源 | 說明 |
|---|---|
| 區塊 | Blockbook 的 bb_getBlock 函式所傳回的物件陣列 |
範例:追蹤高價值交易
以下範例函式會過濾資料流,以追蹤「鯨魚」的活動,方法是僅識別並回傳交易金額為 1 BTC 或以上的交易。
- 篩選功能
- 回應
// Chain: Bitcoin
// Dataset: Block
// Test with block: 927171
function main(payload) {
const {
data,
metadata,
} = payload;
// Define the threshold: 1 BTC in Satoshis (100,000,000 sats = 1 BTC)
const MIN_VALUE = 100000000;
const results = [];
// Iterate through each block in the batch (Streams may deliver multiple blocks at once)
for (const block of data) {
// Iterate through all transactions in the current block
for (const tx of block.txs) {
// Filter logic: Check if the transaction value meets our threshold
if (parseInt(tx.value) >= MIN_VALUE) {
// Construct a simplified custom payload with only relevant details
results.push({
txid: tx.txid,
blockHeight: tx.blockHeight,
blockTime: tx.blockTime,
// Convert Satoshis to BTC for human readability
valueBTC: parseInt(tx.value) / 100000000,
fees: tx.fees
});
}
}
}
// Return the array of high-value transactions, or null to skip the block if none found
return results.length ? results : null;
}
[
{
"blockHeight": 927171,
"blockTime": 1765316511,
"fees": "0",
"txid": "fbc62d0e65f2bed9e193480b33adc6f117ba44c839928becadfb61f817f6e7fb",
"valueBTC": 3.14554746
},
{
"blockHeight": 927171,
"blockTime": 1765316511,
"fees": "5640",
"txid": "494ff35569b01a799db0ef0975923844faa46bd1a57a8625cd44b152ded3b661",
"valueBTC": 6.92443529
}
]
XRP Ledger
Streams 您Streams 從XRP Ledger XRPL)擷取歷史資料,並將完整的帳本物件傳送至您的目標位置。
如需完整的 JSON 規格及各資料集的詳細資訊,請參閱「資料來源」文件。
可用的資料來源
| 資料來源 | 說明 |
|---|---|
| 賬簿 | 由 ledger 函式所回傳的帳簿物件陣列 |
範例:篩選已成功完成的付款
以下範例函式會過濾資料流,僅保留成功的付款交易,並剔除其他交易類型及失敗的嘗試,以確保獲得一套乾淨的資金轉移資料集。
- 篩選功能
- 回應
// Chain: XRP Ledger
// Dataset: Ledger
// Test with ledger: 100768299
function main(payload) {
const results = [];
const {
data,
metadata,
} = payload;
// Iterate through each ledger in the batch (Streams may deliver multiple ledgers at once)
for (const item of data) {
const ledger = item.ledger;
// Loop through all transactions in the current ledger
for (const tx of ledger.transactions) {
// Filter Logic:
// 1. Check if it is a 'Payment' type (standard value transfer)
// 2. Check if the transaction succeeded ('tesSUCCESS')
if (
tx.TransactionType === "Payment" &&
tx.metaData?.TransactionResult === "tesSUCCESS"
) {
// Construct a simplified custom payload with key transfer details
results.push({
hash: tx.hash,
ledgerIndex: ledger.ledger_index,
closeTime: ledger.close_time_iso, // Human-readable timestamp
account: tx.Account, // Sender
destination: tx.Destination, // Receiver
fee: tx.Fee
});
}
}
}
// Return the filtered results, or null to skip the ledger if no payments were found
return results.length ? results : null;
}
[
{
"account": "rUg8ac5ikpTaWk5RPei8xuYkNEyUs53G1i",
"closeTime": "2025-12-09T21:49:31Z",
"destination": "rNxp4h8apvRis6mJf9Sh8C6iRxfrDWN7AV",
"fee": "12",
"hash": "0059A19205DA30084FB42C54C343BED150D8F3EB5443FA2C77B87F540A17D6D7",
"ledgerIndex": "100768299"
},
{
"account": "rUg8ac5ikpTaWk5RPei8xuYkNEyUs53G1i",
"closeTime": "2025-12-09T21:49:31Z",
"destination": "rMqfygR9sbZvWMRqStzUunBXH8Ut5DLfxs",
"fee": "12",
"hash": "099243DEE9C12B90A6080A99FC10EB620CDBB713BF17CC6E5D674176A4308A0A",
"ledgerIndex": "100768299"
}
]
回填技巧
分批作業以加快回填速度
預設情況下Streams 每次Streams 一個區塊。針對歷史資料的補傳作業,增加批次大小(例如每次傳送 10 或 100 個區塊)可降低開銷並加快資料匯入速度。請根據目標系統的容量,在 Streams 設定中調整此參數。
若您持續進行即時串流,建議考慮使用Elastic Batch來自動調整批次大小。
效能優化
將「篩選」(僅傳輸您所需的数据)與「壓縮」(以優化數據傳輸)結合使用,可提升大規模回填作業的效能。
如果您的篩選器傳回 null, 不會將任何資料傳送至您的目的地(以減少不必要的資料量)。注意:Streams 根據處理的區塊數量Streams API 配額,無論是否進行過濾。
除錯篩選器
在開發過程中,請透過篩選函式進行記錄以進行除錯。記錄會顯示在「串流篩選器編輯器」中「結果」分頁旁邊的「記錄」分頁中。
在 JavaScript 中,請使用 console.log(). 在 Go 語言中,請使用 fmt.Println — 其輸出內容記錄於 日誌 將 `tab` 視為資訊級別的日誌,它會自動將物件(地圖、切片、結構體)格式化為字串,因此您可以直接記錄有效載荷。
以下 Go 範例假設一個 有效載荷 在您的篩選器中已宣告了 struct(請參閱上方的範例)。在 篩選 該簽名會導致解釋器回報一則容易引起誤解的 常數定義迴圈 發生錯誤,因此請確保已定義該結構體。
- 去
- JavaScript
func Filter(qn *qn.QNContext, payload Payload) interface{} {
fmt.Println("Data:", payload.Data)
fmt.Println("Metadata:", payload.Metadata)
// ... rest of your filter
}
function main(payload) {
const {
data,
metadata,
} = payload;
console.log("Data:", data)
console.log("Metadata:", metadata)
// ... rest of your filter
}
使用原始資料進行測試
在編寫新篩選器時,您可以從 Stream 配置使用者介面下載特定測試區塊的原始資料。這有助於您在編寫篩選器邏輯之前,先了解確切的資料包結構。
安全性與驗證
Streams 每次傳送Streams 一個安全憑證,您可以藉此驗證請求是否源自Quicknode。您還可以在目標設定中配置自訂標頭。關於 Webhook 目標的實作細節,請參閱《如何驗證傳入的Streams 訊息》。