跳到内容

Web3 Event Scan Starter

受众:开发者 摘要:区块扫描窗口、事件扫描编排、扫描器注册表、检查点持久化、Money Web3 自动扫描调度。

何时使用

场景建议
计算下一段稳定扫描窗口Web3.nextScanWindow(...)
事件扫描编排(读链头 + 查询日志 + 检查点)Web3.scanEvents(...)
注册自己的扫描器(按 scannerNameWeb3EventScannerRegistry
扫描检查点 + 执行日志后台运维/api/v1/admin/web3/scan-checkpoints
事件幂等键 / 参数读取工具Web3EventIdempotencyKeys / Web3EventValues

区块扫描窗口

生产环境做事件扫描时,通常不会直接扫描到最新链头,而是扣除确认数后只扫描稳定区块。Web3BlockScans 用于本地计算下一段扫描窗口,不访问 RPC、不落库、不启动任务:

java
Web3BlockScanWindow window = Web3BlockScans.nextWindow(
        BigInteger.valueOf(18000000),
        Web3.blockNumber("ethereum"),
        12,
        BigInteger.valueOf(1000));

if (window.isAvailable()) {
    String fromBlock = window.getFromBlock().toString();
    String toBlock = window.getToBlock().toString();
    BigInteger nextBlock = window.getNextBlock();
}

需要落库保存游标时,优先使用 Web3BlockScanCheckpoint 表达检查点。Money Web3 已经提供检查点和扫描日志的后台运维入口,业务模块也可以复用同一模型做自己的持久化:

java
Web3BlockScanCheckpoint checkpoint = Web3BlockScans.checkpoint(
        "bsc",
        "erc20-transfer",
        BigInteger.valueOf(18000000));

Web3BlockScanWindow window = Web3BlockScans.nextWindow(
        checkpoint,
        Web3.blockNumber("bsc"),
        12,
        BigInteger.valueOf(1000));

if (window.isAvailable()) {
    // 扫描 window.fromBlock -> window.toBlock。
    checkpoint = Web3BlockScans.advance(checkpoint, window);
    // 将 checkpoint.nextBlock 持久化,供下一轮扫描使用。
}

约定:

  • nextBlock 表示下一次待扫描起始区块,通常来自业务侧保存的游标。
  • latestBlock 表示当前链头区块,调用方可以通过 Web3.blockNumber(chain) 读取。
  • requiredConfirmations 用来扣除不稳定区块,例如链头 120、确认数 12,稳定区块为 108
  • blockRangeSize 用于限制单次扫描跨度;为 null 时一次扫描到稳定区块。
  • 当没有可扫描稳定区块时,available=falsenextBlock 保持不变。
  • advance(...) 只应在本轮扫描成功后调用;扫描失败不要推进检查点。
  • Web3BlockScanCheckpoint 只提供通用模型,不负责落库、锁、重试或业务状态更新。

事件扫描编排

如果业务需要按检查点持续扫描事件,可以使用 Web3.scanEvents(...)。它会自动读取检查点对应链的当前区块高度,计算稳定扫描窗口,把窗口覆盖到 Web3EventLogQuery.fromBlock/toBlock,然后查询事件日志。

java
Web3EventScanResult result = Web3.scanEvents(Web3EventScanRequest.builder()
        .checkpoint(checkpoint)
        .query(Web3EventLogQuery.builder()
                .address(contractAddress)
                .event(transfer)
                .build())
        .requiredConfirmations(12)
        .blockRangeSize(BigInteger.valueOf(1000))
        .build(), context -> {
            for (Web3EventLog log : context.getLogs()) {
                // 在业务 Service 中做金额、地址、链 ID、事件唯一键和幂等校验。
            }
        });

if (result.isScanned()) {
    Web3BlockScanCheckpoint next = result.getNextCheckpoint();
    // 处理器正常返回后,再把 next.nextBlock 持久化。
}

约定:

  • scanEvents(...) 不落库、不加锁、不调度、不更新任何支付或资产业务状态。
  • 只有 handler 正常返回后,返回值里的 nextCheckpoint 才代表本轮可以保存的游标。
  • handler 抛出异常时,异常会向外传播,调用方不应推进数据库检查点。
  • 无稳定区块可扫描时,scanned=false,不会调用事件查询,也不会调用 handler。
  • 多实例扫描、分布式锁、失败重试和任务调度应放在 Scheduler/Queue 或业务模块里完成。

事件扫描器注册表

如果多个业务模块都需要扫描链上事件,推荐实现 Web3EventScanner,由 Spring 自动注册到 Web3EventScannerRegistry。这样 Money Web3 只需要保存检查点上的 scannerName,真实业务查询条件和日志处理逻辑由各业务模块自己提供:

java
@Component
public class Erc20TransferScanner implements Web3EventScanner {

    @Override
    public String scannerName() {
        return "erc20-transfer";
    }

    @Override
    public Web3EventLogQuery query(Web3BlockScanCheckpoint checkpoint) {
        return Web3EventLogQuery.builder()
                .address("0x0000000000000000000000000000000000000002")
                .event(Web3TokenEvents.ERC20_TRANSFER)
                .build();
    }

    @Override
    public void handle(Web3EventScanContext context) {
        for (Web3EventLog log : context.getLogs()) {
            String from = Web3EventValues.text(log, "from");
            BigInteger value = Web3EventValues.bigInteger(log, "value");
            // 在业务 Service 中做事件唯一键、金额、地址、链 ID 和幂等处理。
        }
    }
}

调度任务或业务 Service 可以通过注册表取出扫描器,再复用 Web3.scanEvents(...)

java
Web3EventScanner scanner = scannerRegistry.getRequired(checkpoint.getScannerName());

Web3EventScanResult result = Web3.scanEvents(Web3EventScanRequest.builder()
        .checkpoint(checkpoint)
        .query(scanner.query(checkpoint))
        .requiredConfirmations(12)
        .blockRangeSize(BigInteger.valueOf(1000))
        .build(), scanner::handle);

约定:

  • scannerName 使用小写短横线,例如 erc20-transferweb3-payment-confirmation
  • 注册表按大小写不敏感方式匹配名称,并拒绝重复扫描器名。
  • Web3 starter 只提供扫描器发现和基础编排,不直接落库、不加锁、不启动任务。
  • 扫描任务的启停、分布式锁、失败重试、状态推进和业务幂等仍放在 Scheduler/Queue 或业务 Service 中。

仓库不再维护独立 Web3 样例模块。正式业务扫描器直接注册在业务模块中,例如 Money Web3 的余额充值扫描器会复用 Web3TokenEvents.ERC20_TRANSFER 构建查询,用 Web3EventIdempotencyKeys.contractEventLog(...) 生成业务去重键,并在业务 Service 事务内推进状态。

内置业务扫描器

Money Web3 模块会按业务需要注册少量正式扫描器。它们仍然复用 Web3 starter 的通用扫描编排,业务状态更新放在 Money Web3 Service 中完成。

扫描器用途attributes
user-balance-recharge-erc20-transfer匹配用户余额 Web3 充值的 ERC-20 Transfer 日志Token 合约地址,或 {"contractAddress":"0x..."}

user-balance-recharge-erc20-transfer 只把链上转账事件绑定到待处理充值单:链、Token 合约、平台收款地址和金额全部一致时,写入付款地址、收款地址和交易哈希。它不直接入账、不归集资金、不签名、不广播;到账仍由 Web3 充值确认入口或自动确认任务按确认数推进。

扫描检查点管理

Web3 starter 提供 Web3BlockScanCheckpointWeb3BlockScans 负责本地扫描窗口和游标推进规则;Money Web3 负责把游标落库并提供后台运维入口。

数据库表:

text
money_web3_scan_checkpoint
money_web3_scan_log

后台 API:

http
GET    /api/v1/admin/web3/scan-checkpoints
GET    /api/v1/admin/web3/scan-checkpoints/{id}
GET    /api/v1/admin/web3/scan-checkpoints/{id}/next-window
POST   /api/v1/admin/web3/scan-checkpoints
PUT    /api/v1/admin/web3/scan-checkpoints/{id}
POST   /api/v1/admin/web3/scan-checkpoints/{id}/advance
POST   /api/v1/admin/web3/scan-checkpoints/{id}/pause
POST   /api/v1/admin/web3/scan-checkpoints/{id}/resume
POST   /api/v1/admin/web3/scan-checkpoints/{id}/mark-error
DELETE /api/v1/admin/web3/scan-checkpoints/{id}
DELETE /api/v1/admin/web3/scan-checkpoints
POST   /api/v1/admin/web3/scans/trigger
POST   /api/v1/admin/web3/scans/checkpoints/{id}/trigger
GET    /api/v1/admin/web3/scan-logs
GET    /api/v1/admin/web3/scan-logs/{id}
DELETE /api/v1/admin/web3/scan-logs/{id}
DELETE /api/v1/admin/web3/scan-logs

字段约定:

字段说明
chainName链配置名称,例如 bscethereum
scannerName扫描器名称,例如 erc20-transferweb3-payment-confirmation
nextBlock下一次待扫描区块
lastScannedBlock上次扫描完成区块
latestBlock上次观察到的链头高度
stableBlock按确认数扣除后的稳定区块
requiredConfirmations扫描确认数
blockRangeSize单次扫描最大跨度
caughtUp是否已经追平稳定区块
statusactivepausederror

next-window 会读取当前链头并预览下一次稳定扫描窗口,不触发真实扫描,也不推进游标。

advance 只推进检查点游标,不启动真实链扫描、不更新支付订单、不归档资产流水。pause / resume 用于扫描任务运行前判断是否跳过该检查点;mark-error 用于把失败原因留在检查点上,便于后台排查。

scans/trigger 是后台运维手动触发入口,会立即扫描 active 状态检查点,并返回本轮统计和每个检查点结果:

json
{
  "limit": 10,
  "traceId": "manual-20260520"
}

如果只需要重试某个失败检查点,可以使用单检查点触发入口:

text
POST /api/v1/admin/web3/scans/checkpoints/{id}/trigger

请求体只需要可选的 traceId。该入口会按指定检查点立即执行一次扫描,适合修复 RPC、扫描器或业务 handler 后对单个 error 检查点做精确重跑;它仍然复用同一套扫描日志、失败通知、游标推进和业务 handler 幂等规则。

扫描日志是系统生成的执行记录,只提供分页、详情、单条删除和批量删除,不提供人工创建或修改。自动扫描和手动触发扫描都会记录日志;日志不会影响扫描主流程,日志写入失败只记录应用日志,不会回滚已经成功的扫描和游标推进。

触发扫描结果的单个检查点项会返回 logCountremovedLogCount。其中 removedLogCount 表示本轮查询到的链上日志中,RPC 标记为 removed=true 的数量,通常用于提示疑似链重组或已回滚事件;业务扫描器仍应按自身幂等规则处理日志。

链上事件补偿任务的完整运行边界、幂等、失败处理和上线检查见 Web3 链上事件补偿任务

扫描日志核心字段:

字段说明
checkpointId对应检查点 ID
chainName链配置名称
scannerName扫描器名称
statussuccessskippedfailed
scanned本轮是否实际扫描了稳定区块
logCount本轮查询到的链上日志数量
removedLogCount本轮查询到的 removed=true 日志数量,用于识别疑似链重组
fromBlock / toBlock本轮扫描区块范围
nextBlock本轮结束后建议保存的下一起始区块
latestBlock / stableBlock本轮观察到的链头和稳定区块
traceId手动触发或调度任务传入的追踪标识
startedAt / finishedAt本轮扫描开始和结束时间
durationMs本轮耗时,单位毫秒
errorMessage失败原因,最大保留 1000 字符

权限码:

text
money:web3:chain:view
money:web3:scan-checkpoint:view
money:web3:scan-checkpoint:create
money:web3:scan-checkpoint:update
money:web3:scan-checkpoint:delete
money:web3:scan:execute
money:web3:scan-log:view
money:web3:scan-log:delete

Money Web3 内置一个轻量 Scheduler 扫描任务,默认每 1 分钟扫描一页 active 检查点。任务会根据检查点的 scannerNameWeb3EventScannerRegistry 找到业务扫描器,调用 Web3.scanEvents(...),处理成功后推进游标并记录 successskipped 执行日志;如果扫描器缺失、RPC 失败或业务 handler 抛错,会把检查点标记为 error,保存错误原因,并记录 failed 执行日志。

yaml
spring:
  open:
    money:
      web3:
        enabled: true
        scanner:
          enabled: true
          auto-startup: true
          task-name: money.web3.scan
          instance-id: default
          fixed-delay: 1m
          limit: 50
          failure-notification:
            enabled: true
            channels:
              - log
            subject: Web3 scan failed
            recipient-type:
            recipient-value:

约定:

  • 自动扫描只处理 active 状态检查点;pausederror 不会自动执行。
  • limit 控制单轮最多扫描检查点数量,避免一次任务压垮 RPC 节点。
  • 自动扫描会按 lastScannedAtid 排序读取检查点,降低检查点数量较多时前几条反复占用扫描窗口的概率。
  • failure-notification 复用 Notification starter,默认走 log 通道;如果改为 databasemailsmswebhook 等通道,应同步配置 Notification 对应通道和接收人。
  • Scheduler Provider 负责任务级多实例协调;业务 handler 仍要做事件唯一键、金额、地址、链 ID 和本地状态幂等。
  • Web3 starter 不直接写支付订单或资产流水,所有业务状态更新必须留在业务 Service 中。

业务 handler 做事件幂等时,优先使用 Web3EventIdempotencyKeys 生成统一去重键;读取事件字段时优先使用 Web3EventValues

java
String key = Web3EventIdempotencyKeys.transactionLog("bsc", eventLog);
String scopedKey = Web3EventIdempotencyKeys.contractEventLog("bsc", eventLog);
String from = Web3EventValues.text(eventLog, "from");
BigInteger value = Web3EventValues.bigInteger(eventLog, "value");

transactionLog 使用 chainName + transactionHash + logIndex,适合作为链上日志全局唯一键;contractEventLog 额外加入合约地址和事件签名,适合一个业务表同时归档多类合约事件。业务表应对该 key 建唯一索引,handler 成功落库后再允许推进检查点。

钱包登录流程本身仍由 IAM 管理,EVM 验签器由 Web3 starter 提供:

yaml
spring:
  open:
    web3:
      wallet:
        enabled: true
        default-chain-type: evm
        challenge-ttl: 5m
        auto-create-user: true

相关

Released under the Apache License 2.0.