Solana区块链以顺序、仅追加的账本存储数据。这对数据完整性和交易吞吐量有益,但代价是:查询历史数据非常低效且速度极慢。
复杂操作通常涉及从多个来源过滤、聚合或联合数据。在这些情况下,直接查询Solana对于大多数实际应用来说是不切实际的。
为了解决这个问题,大多数企业构建了Solana历史数据的私有索引。
索引Solana数据意味着什么?
索引是指从Solana区块链查询数据并将其存储在后端数据库(如PostgreSQL、ClickHouse)中,然后可用来满足客户请求,而无需直接使用Solana RPC调用查询区块链。
索引器通常做四件事:
- 回填历史数据: 使用存档RPC方法查询所有历史数据
- 流式传输新数据: 处理网络确认的新区块
- 解析和转换数据: 从确认的区块中提取相关数据(如交易、状态变化等)
- 将数据组织到数据库中: 使用新数据更新索引
为什么大多数公司构建Solana索引?
公司构建Solana索引是因为他们的业务依赖于提供快速、实时访问特定用途的区块链数据,而原生RPC无法提供这些数据(例如,NFT销售历史)。
公司还利用自定义索引将链下数据(如集中交易所价格、KYC信息等)与链上数据结合使用。
钱包示例
例如,如果一个Solana钱包需要快速返回用户的代币账户和余额,直接通过getTokenAccountsByOwner和getTokenAccountBalance查询Solana太慢,并可能导致产品无法使用。因此,钱包通常会维护自己的客户地址、代币和账户余额索引。
交易示例
类似地,加密交易公司可能希望记录在特定交易对(例如,SOL-USDC)或特定市场上发生的所有交易活动,以便对其交易算法进行回测。
直接查询区块链来获取这些数据对于任何实际的交易分析来说都太慢了。相反,量化交易员可能会选择为SOL-USDC市场构建索引,并使用诸如LaserStream之类的实时流产品持续更新最新交易。
过滤示例
想象一下,用户希望在其前端应用中按照特定标准过滤交易(例如,按代币类型、转账金额、日期或钱包地址)。
没有索引器,您的应用需要扫描成千上万个区块中的数百万笔交易,检查每一笔是否符合过滤条件。
这个过程对于现代产品用户体验来说太慢了。
盈亏示例
要计算交易员的盈亏(PnL),您需要:
- 找出在给定时间范围内与其钱包相关的所有交易
- 过滤掉兑换交易并将其标记为买入或卖出
- 确定用户在每次兑换中支付的费用
- 获取每次交易时每种代币的历史价格数据
- 汇总每笔交易的盈亏以计算交易员的总盈亏
实时计算这一切是不现实的,需要一个更快、更具可扩展性的解决方案。
通过索引,所有这些信息已经被处理并存储在一个可查询的数据库中。现在,计算交易者的 PnL 只需一个即时提供的 API 调用。
让我们看看为 Solana 索引进行回填并保持其最新的三种方法。
步骤 1:获取历史数据
构建 Solana 索引的第一步是获取您所关心的所有历史数据。
主要有三种方法:
- getTransactionsForAddress (推荐)
- getSignaturesForAddress 和 getTransaction
- getBlock
方法 1:getTransactionsForAddress(推荐)
getTransactionsForAddress RPC方法允许您获取区块链数据任意段落的完整交易详情。由于其强大的过滤功能,您不会浪费时间检索不需要的数据,而且由于其反向搜索功能,您可以按时间顺序获取交易。
使用此方法的步骤
- 确定所需数据的时间范围并相应设置过滤器
- 设置
transactionDetails为full以获取所有交易详情
- 如果需要,配置
tokenAccounts过滤器以包含相关的代币账户交易
- 使用
paginationToken分页浏览结果
- 在每次迭代中,提取所需数据并存储在数据库中
使用 getTransactionsForAddress 的好处
使用gTFA endpoint的主要优势是快速和简便。通过slot和基于时间的过滤器、代币账户支持、反向搜索和分页,您可以在Solana的历史记录中通过单次调用获取所需的任何数据,无需复杂的循环或重试逻辑。与getSignaturesForAddress不同,它还可以包括与地址关联的代币账户相关的交易。
方法 2:getSignaturesForAddress 和 getTransaction
在发布 gTFA 之前,查询历史数据的标准方法是递归地循环使用 getSignaturesForAddress 从最新到最老的签名,然后调用 getTransaction 来提取完整的交易详情。
使用此方法的步骤
以下是使用此方法的基本步骤:
- 调用
getSignaturesForAddress
- 存储此调用中最后接收到的交易的签名
- 对于下一次调用
getSignaturesForAddress,将 before 参数设置为此签名
- 按需重复此循环
- 对于以这种方式检索到的每个交易签名,调用
getTransaction 以获取其完整的交易详情
- 将相关数据插入你的数据库中
此方法的缺点
不幸的是,要使用此方法,您需要:
- 从最新的交易开始并向后工作
- 对每笔交易多进行一次 RPC 调用
- 构建一个线程安全队列来处理并发处理
- 构建重试和退避逻辑以防止数据丢失和速率限制
- 不包括涉及由地址拥有的关联代币账户的交易
虽然这种方法有效,但更复杂,灵活性较差,并且消耗更多的 credits。要获取包括代币账户在内的完整钱包历史,请改用 getTransactionsForAddress。
方法 3:使用 getBlock
getBlock 方法在目标区块中高比例交易与分析相关时最为有效,例如索引 经常使用的 Solana 程序 的交易,如 DFlow 的 Aggregator、Pump.fun 程序或 Solana 的 Token 程序。
使用此方法的步骤
使用 getBlock 查询历史数据的基本过程包括:
- 决定要查询的时间范围
- 将此时间范围转换为槽编号
- 顺序获取相应的区块(前向或后向)
- 对于每个区块,筛选与您的索引相关的交易
- 将其中的相关信息存储在您的索引中
对于大多数用例来说,该方法本质上是浪费的,因为您在获取区块中的所有交易时,通常只有很小一部分与您的分析相关。
仅在检查频繁使用的程序的交易或地址过滤无法获取目标数据时使用此方法。
步骤 2:将 Solana 数据与您的数据库同步
获取历史数据后,您需要对其进行转换并有效地存储在数据库中。
您的存储选择应该针对您的具体用例量身定制 —— 没有一种通用的解决方案。适合的数据库取决于数据集的大小、延迟要求、查询模式和团队专业知识。
选项 1:SQL 数据库
对于大多数用例,建议将 Solana 数据存储在关系数据库如 PostgreSQL 中。SQL 灵活、普遍且易于学习。现代关系数据库可以扩展到超过 1 亿行,同时仍为您提供 ACID 合规性、复杂联接和强大的二级索引的好处。
使用 SQLite 进行原型设计、本地开发或当您想要使用单文件数据库进行零配置时。它非常适合数据集保持在几 GB 以下的情况。
对于需要数据复制、多个客户端并发访问或者高级功能(如全文搜索和 JSON 操作符)的生产应用程序,使用 PostgreSQL。
对于大多数生产级别的 Solana 索引器,我们推荐使用 PostgreSQL。
实现示例:
作为示例,我们将展示如何在 PostgreSQL 数据库中存储令牌转移。
首先,创建一个表:
CREATE TABLE token_transfers (
id BIGSERIAL PRIMARY KEY,
slot BIGINT NOT NULL,
timestamp TIMESTAMP NOT NULL,
signature BYTEA NOT NULL UNIQUE,
token_mint BYTEA NOT NULL,
source_address BYTEA NOT NULL,
destination_address BYTEA NOT NULL,
amount BIGINT NOT NULL,
decimals SMALLINT NOT NULL,
program_id BYTEA
);
然后,对经常查询的列添加索引:
CREATE INDEX idx_source_address ON token_transfers (source_address);
CREATE INDEX idx_destination_address ON token_transfers (destination_address);
CREATE INDEX idx_token_mint ON token_transfers (token_mint);
如果只有部分数据经常被查询,也可以创建部分索引。
以下是如何仅为高价值转移创建索引:
CREATE INDEX idx_large_transfers ON token_transfers(amount) WHERE amount > 1000000;
在回填数据时,确保使用批量插入和准备好的语句以获得最佳写入速度。
选项 2:列式数据库
列式数据库针对分析查询、聚合和高容量时间序列数据进行了优化。如果需要索引数十亿笔交易,像 ClickHouse 或 Cassandra 这样的列式数据库是最佳选择。
当您需要对大数据集进行实时分析查询时,使用 ClickHouse——它针对快速读取、聚合和时间序列分析进行了优化。
当您需要极高的写入吞吐量、轻松的水平扩展和高容错性时,使用 Cassandra。这使得它非常适合持续摄取大量 Solana 数据。
实现示例:
我们将展示如何在 ClickHouse 数据库中存储令牌转移。
为此,创建一个使用 MergeTree 表引擎的表。它设计用于高摄取速率,因此非常适合索引。
使用以下命令:
CREATE TABLE token_transfers (
block_time DateTime,
slot UInt64,
signature FixedString(64),
token_mint FixedString(32),
source_address FixedString(32),
destination_address FixedString(32),
amount UInt64,
decimals UInt8,
program_id FixedString(32),
date Date DEFAULT toDate(block_time)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(date)
ORDER BY (token_mint, date)
SETTINGS index_granularity = 8192;
在这个设置中,(token_mint, date) 被设置为主键和排序键。ClickHouse 将根据你的排序键在磁盘上排序数据。这对于查询单个 token mint 并按日期范围缩小响应范围是最佳的。
这是一个查询示例:
SELECT date, signature, source_address, destination_address, amount
FROM token_transfers
WHERE token_mint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
AND block_time BETWEEN '2025-01-01' AND '2025-01-31'
交易签名和地址使用 FixedString(N) 数据格式存储,精确存储 N 字节。ClickHouse 自动压缩数据,从而将存储成本降低 10-20 倍并提高查询性能。
为了优化查询性能,使用物化视图来预计算常见的聚合。
例如,可以预计算代币的每日转移量,以便在仪表板上的与量相关的图表中使用。
选项 3:数据湖
数据湖是存储大量原始和处理后的区块链数据的理想选择,用于长期存档和分析查询。
一个简单的实现是使用 Parquet 数据格式与 Amazon Athena。
Parquet 是一种面向列的数据文件格式,旨在实现高效的数据存储和检索。
Amazon Athena 是一种交互式查询服务,允许您使用标准 SQL 分析存储在 Amazon S3 中的数据,而无需设置基础设施或将数据加载到一个单独的数据库中。
仅当需要查询大量非结构化数据时才推荐使用数据湖。对于大多数用例,我们建议使用 SQL 数据库(选项 1)。
实施示例:
我们想要创建一个代币转移的存档并查询它们。
首先,我们需要将它们存储在S3中:创建一个命名为solana_index的bucket,并使用此键结构按时间对您的令牌转移数据进行分区:
s3://solana_index/token_transfers/YYYY/MM/DD/part-00000.parquet
每日的转移存储在相应日期文件夹中的单独 Parquet 文件中。
当您处理 Solana 的转移时,将它们转换为 Parquet 格式并写入相应的 S3 对象。
稍后,您在 Athena 中创建一个表并将其连接到存储桶。这允许您直接在存储桶中的数据上运行这样的查询:
SELECT block_time, signature, source_address, destination_address, amount
FROM token_transfers
WHERE token_mint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' AND block_time BETWEEN TIMESTAMP '2025-01-01 00:00:00' AND TIMESTAMP '2025-01-31 23:59:59';
使用索引框架
使用Carbon和类似框架,以避免编写样板代码,并在数小时而不是数天内设置索引器。
主要功能:
- 为流行程序(Token 程序、DeFi 协议、Metaplex)预构建解码器
- 可配置的数据源(RPC、LaserStream、增强 WebSockets)
- 内建对回填和实时流的支持
- 输出到多个存储后端(开箱即用的 Postgres)
- 完全可定制:您可以设置自己的数据源、解码器和数据接收器
步骤 3:保持索引最新
在回填历史数据后,您需要一个实时流解决方案,以便让索引跟踪新的区块链活动。否则,您的索引将变得陈旧。
方法 1:LaserStream(推荐)
我们推荐LaserStream gRPC作为所有生产索引用例的默认选择。它专为可靠、超低延迟和容错数据流而设计。
使用 LaserStream 的一些好处包括:
- 24 小时历史重播:如果索引器断开连接,LaserStream 会自动重播您未处理的所有事务
- 自动重新连接:我们的LaserStream SDK(Rust、Go、JS/TS)无缝处理网络中断
- 节点故障转移:您的 LaserStream 连接同时从多个节点汇聚数据,确保最大正常运行时间
结合速度和可靠性,LaserStream 非常适合实时应用,如实时交易馈送、交易仪表盘和即时余额更新。
如何使用 LaserStream 进行索引
使用subscribe方法订阅区块链事件。
以下是一些最佳实践:
- 尽量缩小您的过滤器范围:仅订阅您实际需要索引的数据,以最小化带宽消耗和处理需求。
- 使用
confirmed承诺级别:这在延迟和最终性之间取得平衡。processed级别可能不够可靠,而finalized增加了~13秒的延迟。
- 设置
failed: false,除非您特别需要跟踪失败的交易。
- 排除投票交易(
vote: false),因为它们与索引无关。
让我们看看一个例子。
使用以下订阅来索引所有新的代币转移:
{
transactions: {
"transfers": {
vote: false,
failed: false,
accountsInclude: [
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
]
}
},
commitment: CommitmentLevel.CONFIRMED,
accounts: {},
slots: {},
transactionsStatus: {},
blocks: {},
blocksMeta: {},
entry: {},
accountsDataSlice: []
}
方法2:使用增强WebSockets
增强WebSockets 由与 LaserStream 相同的基础设施提供支持,是 LaserStream gRPC 的一种有效的实时流替代方案。
应在以下情况下使用增强型 WSS:
- 您的应用可以忍受偶尔的数据差距
- 实时更新很重要,但不是关键任务
- 您拥有可检测和弥补数据缺失的现有基础设施
- 预算限制很重要,需要最大限度地减少流媒体成本
- 您正在原型或测试,在承诺使用 LaserStream 之前
然而,选择增强 WSS 时需要考虑一些权衡:
- 速度:增强 WSS 很快,但仍然比 LaserStream 慢
- 可靠性:没有历史重播保证。如果您的 WebSocket 断开,您需要使用 RPC 方法手动检测并弥补数据差距
- 复杂性:需要额外的监控基础设施以确保数据完整性
如何使用增强的 WebSockets 进行索引
要更新存储所有令牌转移的索引,您可以像这样订阅transactionSubscribe:
{
jsonrpc: '2.0',
id: 1,
method: 'transactionSubscribe',
params: [
{
failed: false,
accountInclude: [
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
]
},
{
commitment: 'confirmed',
encoding: 'jsonParsed',
transactionDetails: 'full',
maxSupportedTransactionVersion: 0
}
]
}
开始使用
构建一个强大的 Solana 索引并填充数据需要解决三个核心挑战:
- 高效获取历史数据
- 转换和存储数据以便快速检索
- 实时更新已索引的 Solana 数据
通过我们新的最先进的归档系统、getTransactionForAddress 等归档调用,以及行业领先的数据流解决方案如 LaserStream,构建 Solana 索引比以往更加简便和实用。
下一步: