跳到内容

Web3 Token Starter

受众:开发者 摘要:ERC-20 / ERC-721 / ERC-1155 只读 + 写交易辅助、NFT Transfer 查询、Token 批量编排、金额换算。

何时使用

场景建议
ERC-20 基础信息 / 余额 / 授权 / Transfer 查询Web3.erc20(...)
ERC-721 / ERC-1155 基础 / 余额 / URI / 授权Web3.erc721(...) / erc1155(...)
NFT Transfer 事件查询Web3.nftTransfers(...)
ERC-20 transfer / approve 写交易Web3.erc20WriteTransfer(...) / erc20WriteApprove(...)
NFT 写交易(approve / setApprovalForAll / safeTransferFromWeb3.erc721Write(...) / erc1155Write(...)
Token 批量写交易编排(顺序 nonce、失败跳过、可选广播)Web3.tokenBatch(...)
原始单位 / 显示单位金额换算Web3Amounts

ERC-20 只读查询与 Transfer 事件

Token 常规只读查询通过 Web3TokenClient 完成。它内部仍然复用 Web3Manager.contractCall(...),不会泄露 web3j 类型到业务模块:

java
@RequiredArgsConstructor
public class TokenQueryService {

    private final Web3TokenClient tokenClient;

    public Web3TokenInfo info(String contractAddress) {
        return tokenClient.erc20Info("ethereum", contractAddress);
    }

    public BigInteger balance(String contractAddress, String ownerAddress) {
        return tokenClient.erc20BalanceOf("ethereum", contractAddress, ownerAddress);
    }

    public BigInteger allowance(String contractAddress, String ownerAddress, String spenderAddress) {
        return tokenClient.erc20Allowance("ethereum", contractAddress, ownerAddress, spenderAddress);
    }

    public List<Web3TokenTransfer> transfers(String contractAddress, BigInteger fromBlock, BigInteger toBlock) {
        return tokenClient.erc20Transfers("ethereum", Web3TokenTransferQuery.builder()
                .contractAddress(contractAddress)
                .fromBlock(fromBlock.toString())
                .toBlock(toBlock.toString())
                .blockRangeSize(BigInteger.valueOf(1000))
                .build());
    }

}

当前支持的 ERC-20 只读字段:

方法说明
erc20Info(chain, contractAddress)查询 namesymboldecimalstotalSupply
erc20BalanceOf(chain, contractAddress, ownerAddress)查询指定地址余额,返回原始最小单位
erc20Allowance(chain, contractAddress, ownerAddress, spenderAddress)查询 owner 授权给 spender 的额度,返回原始最小单位
erc20Transfers(chain, query)查询 ERC-20 Transfer(address,address,uint256) 事件
erc20Transfer(chain, request)生成 ERC-20 transfer(address,uint256) 写交易签名,可选广播
erc20Approve(chain, request)生成 ERC-20 approve(address,uint256) 写交易签名,可选广播

namesymbol 优先按标准 string 读取;如果旧合约返回 bytes32,会自动降级读取并裁剪末尾空字节。erc20Allowance(...) 只读取链上授权额度;需要授权交易时使用 erc20Approve(...),默认只签名不广播。erc20Transfers(...) 基于 eth_getLogs,支持按区块范围、合约地址、fromAddresstoAddress 过滤。

常见 Token 事件定义统一放在 Web3TokenEvents

java
Web3EventLogQuery query = Web3EventLogQuery.builder()
        .address(contractAddress)
        .event(Web3TokenEvents.ERC20_TRANSFER)
        .build();

当前内置 ERC20_TRANSFERERC721_TRANSFERERC1155_TRANSFER_SINGLEERC1155_TRANSFER_BATCH。业务扫描器直接复用这些定义,不要重复手写常见 Token 事件 ABI。

生产环境扫描历史 Transfer 时,建议设置 blockRangeSize,让客户端把一个大区块范围拆成多个小的 RPC 请求:

java
Web3TokenTransferQuery query = Web3TokenTransferQuery.builder()
        .contractAddress("0x0000000000000000000000000000000000000002")
        .fromBlock("18000000")
        .toBlock("18050000")
        .blockRangeSize(BigInteger.valueOf(1000))
        .build();

拆分规则:

  • blockRangeSize 表示单次 RPC 最多查询多少个区块,包含起止区块。
  • fromBlock / toBlock 为数字、十六进制数字或 latest 时可拆分;latest 会先解析为当前区块高度。
  • safefinalizedpending 等动态标签不自动拆分,会退回单次 eth_getLogs
  • fromAddress / toAddress 等事件过滤地址会先通过 EvmAddresses.require(...) 校验,避免无效地址或过长地址被错误编码为 topic。
  • 该策略只减少 RPC 超时和节点限制风险,不保存扫描游标;游标重扫和多实例补偿应放在 Scheduler 或业务模块中实现。

ERC-20 写交易辅助只覆盖 transferapprove,不提供 transferFrom、订单结算或本地资产流水修改。批量 Token/NFT 写交易请使用 batchTransactions(...) 做顺序 nonce 编排;链上支付结算由 Payment / Money Web3 复核结算链路在事务幂等边界内处理。

ERC-721 只读查询

NFT 常规只读查询同样通过 Web3TokenClient 完成,内部仍然复用合约只读调用:

java
Web3NftInfo info = tokenClient.erc721Info("ethereum", contractAddress);
BigInteger balance = tokenClient.erc721BalanceOf("ethereum", contractAddress, ownerAddress);
String owner = tokenClient.erc721OwnerOf("ethereum", contractAddress, BigInteger.ONE);
String tokenUri = tokenClient.erc721TokenUri("ethereum", contractAddress, BigInteger.ONE);
String approved = tokenClient.erc721GetApproved("ethereum", contractAddress, BigInteger.ONE);
boolean approvedForAll = tokenClient.erc721IsApprovedForAll("ethereum", contractAddress, ownerAddress,
        operatorAddress);
boolean supportsErc721 = tokenClient.supportsInterface("ethereum", contractAddress, Web3TokenStandards.ERC721);
Web3NftStandardSupport standardSupport = tokenClient.nftStandardSupport("ethereum", contractAddress);

当前支持:

方法说明
supportsInterface(chain, contractAddress, interfaceId)通过 ERC-165 查询合约是否支持指定 4 字节 interface ID
nftStandardSupport(chain, contractAddress)查询 ERC-165、ERC-721、ERC-721 Metadata、ERC-721 Enumerable、ERC-1155 和 ERC-1155 Metadata URI 支持情况
erc721Info(chain, contractAddress)查询 namesymbol
erc721BalanceOf(chain, contractAddress, ownerAddress)查询指定地址持有数量
erc721OwnerOf(chain, contractAddress, tokenId)查询指定 token ID 的持有人
erc721TokenUri(chain, contractAddress, tokenId)查询指定 token ID 的元数据 URI
erc721GetApproved(chain, contractAddress, tokenId)查询指定 token ID 的授权地址
erc721IsApprovedForAll(chain, contractAddress, ownerAddress, operatorAddress)查询 owner 是否已给 operator 全量授权
erc721Approve(chain, request)生成 approve(address,uint256) 写交易签名,可选广播
erc721SetApprovalForAll(chain, request)生成 setApprovalForAll(address,bool) 写交易签名,可选广播
erc721TransferFrom(chain, request)生成 transferFrom(address,address,uint256) 写交易签名,可选广播
erc721SafeTransferFrom(chain, request)生成 safeTransferFrom 写交易签名,可选广播

常用 interface ID 统一放在 Web3TokenStandards,不要在业务里散落 0x80ac58cd 这类魔法字符串。supportsInterface(...) 只调用标准 supportsInterface(bytes4) 只读方法;如果合约没有实现 ERC-165,节点可能返回调用失败,业务侧应按具体场景处理。

真实业务使用 tokenURI 后,还需要业务侧自行下载和校验元数据内容、媒体文件类型、缓存策略和外部资源安全边界。ERC-721 写交易辅助只负责 ABI data、签名和可选广播,不负责本地资产归属变更。

ERC-1155 只读查询

ERC-1155 多 Token 合约同样通过 Web3TokenClient 查询。批量查询会自动编码 address[]uint256[],业务代码不需要手写 ABI 数组参数:

java
BigInteger balance = tokenClient.erc1155BalanceOf(
        "ethereum",
        contractAddress,
        ownerAddress,
        BigInteger.ONE);

List<BigInteger> balances = tokenClient.erc1155BalanceOfBatch(
        "ethereum",
        contractAddress,
        List.of(ownerAddress, anotherOwnerAddress),
        List.of(BigInteger.ONE, BigInteger.TWO));

String uri = tokenClient.erc1155Uri("ethereum", contractAddress, BigInteger.ONE);
boolean approvedForAll = tokenClient.erc1155IsApprovedForAll("ethereum", contractAddress, ownerAddress,
        operatorAddress);

当前支持:

方法说明
erc1155BalanceOf(chain, contractAddress, ownerAddress, tokenId)查询指定地址、指定 token ID 的持有数量
erc1155BalanceOfBatch(chain, contractAddress, ownerAddresses, tokenIds)批量查询地址和 token ID 的持有数量,返回顺序与输入顺序一致
erc1155Uri(chain, contractAddress, tokenId)查询指定 token ID 的 URI 模板或实际 URI
erc1155IsApprovedForAll(chain, contractAddress, ownerAddress, operatorAddress)查询 owner 是否已给 operator 全量授权
erc1155SetApprovalForAll(chain, request)生成 setApprovalForAll(address,bool) 写交易签名,可选广播
erc1155SafeTransferFrom(chain, request)生成 safeTransferFrom(address,address,uint256,uint256,bytes) 写交易签名,可选广播
erc1155SafeBatchTransferFrom(chain, request)生成 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) 写交易签名,可选广播

ownerAddressestokenIds 必须数量一致;数量不一致会抛出 Web3 业务异常。ERC-1155 写交易辅助只负责 ABI data、签名和可选广播,不负责本地资产归属变更。

NFT Transfer 事件查询

NFT 转移事件也通过 Web3TokenClient 查询,内部复用 eth_getLogs 和 ABI 事件解码。常见用法:

java
List<Web3NftTransfer> erc721Transfers = tokenClient.erc721Transfers("ethereum",
        Web3NftTransferQuery.builder()
                .contractAddress(contractAddress)
                .fromBlock("18000000")
                .toBlock("18001000")
                .fromAddress(fromAddress)
                .toAddress(toAddress)
                .tokenId(BigInteger.ONE)
                .blockRangeSize(BigInteger.valueOf(1000))
                .build());

List<Web3NftTransfer> erc1155Singles = tokenClient.erc1155TransferSingles("ethereum", query);
List<Web3NftTransfer> erc1155Batches = tokenClient.erc1155TransferBatches("ethereum", query);

当前支持:

方法说明
erc721Transfers(chain, query)查询 ERC-721 Transfer(address,address,uint256) 事件,支持 fromAddresstoAddresstokenId topic 过滤
erc1155TransferSingles(chain, query)查询 ERC-1155 TransferSingle(address,address,address,uint256,uint256) 事件,支持 operatorAddressfromAddresstoAddress topic 过滤
erc1155TransferBatches(chain, query)查询 ERC-1155 TransferBatch(address,address,address,uint256[],uint256[]) 事件,支持 operatorAddressfromAddresstoAddress topic 过滤

NFT Transfer 查询同样支持 blockRangeSize,可将大区块范围拆成多个小范围 RPC 请求。operatorAddressfromAddresstoAddress 等事件过滤地址会先通过 EvmAddresses.require(...) 校验。ERC-1155 的 id / ids 不是 indexed topic,无法像 ERC-721 tokenId 一样在链节点侧直接过滤,需要业务侧按返回结果再筛选。

ERC-20 写交易辅助

ERC-20 transferapprove 通过 Web3TokenClient 生成 ABI data,再复用 Web3 starter 的签名 / 广播边界。业务模块不需要手写 ABI,也不直接依赖 web3j。

java
Web3TokenTransactionResult transfer = tokenClient.erc20Transfer("bsc",
        Web3Erc20TransferRequest.builder()
                .privateKey("...")
                .fromAddress("0x...")
                .contractAddress("0x...")
                .toAddress("0x...")
                .amount(new BigInteger("1000000"))
                .broadcast(false)
                .build());

Web3TokenTransactionResult approve = tokenClient.erc20Approve("bsc",
        Web3Erc20ApproveRequest.builder()
                .privateKey("...")
                .fromAddress("0x...")
                .contractAddress("0x...")
                .spenderAddress("0x...")
                .amount(BigInteger.ZERO)
                .broadcast(false)
                .build());

约定:

  • amount 始终使用 Token 最小单位,不自动读取 decimals 做换算;业务金额换算优先使用 Web3TokenAmounts
  • transfer 要求 amount > 0approve 允许 amount = 0,用于撤销授权。
  • 默认 broadcast=false,只返回 signedTransaction 和 ABI data;显式 broadcast=true 才会广播链上交易。
  • noncegasPricegasLimitchainId 可显式传入;未传时沿用底层 EVM 链客户端的兜底逻辑。
  • 私钥只作为本次签名请求参数使用,不持久化、不打印、不写日志。
  • 该能力只负责构造、签名和可选广播链上交易,不修改支付订单、用户资产或本地流水。

NFT / ERC-1155 写交易辅助

ERC-721 和 ERC-1155 常见写交易同样通过 Web3TokenClient 生成 ABI data,并复用签名 / 广播边界:

java
Web3TokenTransactionResult approval = tokenClient.erc721SetApprovalForAll("bsc",
        Web3TokenApprovalForAllRequest.builder()
                .privateKey("...")
                .fromAddress("0x...")
                .contractAddress("0x...")
                .operatorAddress("0x...")
                .approved(true)
                .broadcast(false)
                .build());

Web3TokenTransactionResult transfer = tokenClient.erc1155SafeBatchTransferFrom("bsc",
        Web3Erc1155BatchTransferRequest.builder()
                .privateKey("...")
                .fromAddress("0x...")
                .contractAddress("0x...")
                .toAddress("0x...")
                .tokenIds(List.of(BigInteger.ONE, BigInteger.TWO))
                .amounts(List.of(BigInteger.TEN, BigInteger.valueOf(20)))
                .broadcast(false)
                .build());

当前支持:

方法说明
erc721Approve(chain, request)生成 ERC-721 approve(address,uint256) 写交易签名,可选广播
erc721SetApprovalForAll(chain, request)生成 ERC-721 setApprovalForAll(address,bool) 写交易签名,可选广播
erc721TransferFrom(chain, request)生成 ERC-721 transferFrom(address,address,uint256) 写交易签名,可选广播
erc721SafeTransferFrom(chain, request)生成 ERC-721 safeTransferFrom 写交易签名;传入 data 时使用四参数重载
erc1155SetApprovalForAll(chain, request)生成 ERC-1155 setApprovalForAll(address,bool) 写交易签名,可选广播
erc1155SafeTransferFrom(chain, request)生成 ERC-1155 safeTransferFrom(address,address,uint256,uint256,bytes) 写交易签名,可选广播
erc1155SafeBatchTransferFrom(chain, request)生成 ERC-1155 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) 写交易签名,可选广播

约定:

  • fromAddress 表示签名发起地址;转账入参里的持有人地址使用 ownerAddress,为空时默认使用 fromAddress
  • ERC-721 tokenId 允许为 0,但不能为负数。
  • ERC-1155 单笔和批量转账的 amount / amounts 必须大于 0,tokenIdsamounts 数量必须一致。
  • ERC-1155 data 为空时自动使用 0x
  • 所有 NFT / ERC-1155 写交易辅助默认只签名不广播,不修改本地订单、资产或流水。

Token 批量写交易编排

如果需要一次生成多笔 Token/NFT 写交易计划,使用 batchTransactions(...)。它不会把多笔转账强行合并成一个合约调用,而是按条目顺序复用单笔写交易能力,统一处理 nonce、签名、可选广播、失败跳过和统计:

java
Web3TokenBatchTransactionResult result = tokenClient.batchTransactions("bsc",
        Web3TokenBatchTransactionRequest.builder()
                .privateKey(privateKey)
                .fromAddress(operatorAddress)
                .startNonce(BigInteger.valueOf(10))
                .broadcast(false)
                .items(List.of(
                        Web3TokenBatchTransactionItem.builder()
                                .action(Web3TokenTransactionActions.ERC20_TRANSFER)
                                .contractAddress(usdtAddress)
                                .toAddress(receiverAddress)
                                .amount(BigInteger.valueOf(1_000_000))
                                .build(),
                        Web3TokenBatchTransactionItem.builder()
                                .action(Web3TokenTransactionActions.ERC721_SAFE_TRANSFER_FROM)
                                .contractAddress(nftAddress)
                                .toAddress(receiverAddress)
                                .tokenId(BigInteger.ONE)
                                .build()))
                .build());

当前支持的批量动作:

动作常量说明
ERC20_TRANSFERERC-20 transfer(address,uint256)
ERC20_APPROVEERC-20 approve(address,uint256)
ERC721_APPROVEERC-721 approve(address,uint256)
ERC721_SET_APPROVAL_FOR_ALLERC-721 setApprovalForAll(address,bool)
ERC721_TRANSFER_FROMERC-721 transferFrom(address,address,uint256)
ERC721_SAFE_TRANSFER_FROMERC-721 safeTransferFrom
ERC1155_SET_APPROVAL_FOR_ALLERC-1155 setApprovalForAll(address,bool)
ERC1155_SAFE_TRANSFER_FROMERC-1155 safeTransferFrom(address,address,uint256,uint256,bytes)
ERC1155_SAFE_BATCH_TRANSFER_FROMERC-1155 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)

批量编排规则:

  • privateKeyfromAddress 是批量请求级配置,单条 item 不单独携带私钥。
  • startNonce 为空时,第一笔由链客户端按 pending 查询,后续使用已签名 nonce 顺序递增。
  • 单条 item 显式配置 nonce 时,以单条为准。
  • 单条失败不会中断整批,结果会标记为 skipped,用于后台展示和人工复核。
  • broadcast=false 适合后台复核和离线签名计划;broadcast=true 才会逐笔广播。
  • 批量入口只编排链上写交易,不修改本地订单、资产或流水。

Web3 Token 金额换算

Web3 starter 提供 Web3TokenAmounts,用于业务金额和链上原始最小单位之间转换。该工具不访问 RPC,也不处理私钥,只负责精度换算:

java
BigInteger amountRaw = Web3TokenAmounts.toRaw(new BigDecimal("1.23"), 18);
BigDecimal amount = Web3TokenAmounts.fromRaw(amountRaw, 18);
String text = Web3TokenAmounts.formatRaw(amountRaw, 18);

BigInteger wei = Web3TokenAmounts.toWei(new BigDecimal("0.01"));
String bnb = Web3TokenAmounts.formatWei(wei);

toRaw 使用精确换算;如果业务金额的小数位超过 Token decimals,会抛出 ArithmeticException,避免静默截断金额。支付创建上下文、支付确认、转账事件匹配和结算执行里的 amountRaw 都应复用这个工具。

匹配器只返回分析结果,不修改订单。mismatchReasons 使用稳定原因码:

原因码说明
transfer-missing未找到可匹配的转账事件
transfer-removed事件已被链重组移除
chain-mismatch链名称不一致
transaction-hash-mismatch交易 Hash 不一致
receiver-address-mismatch收款地址不一致
token-contract-mismatchToken 合约不一致
amount-mismatch原始最小单位金额不一致
transfer-query-failed查询链上转账事件失败

Money Web3 只读确认接口已经接入这个 matcher:只要支付订单 attributes 里能解析到 receiverAddresstokenContract 和交易区块号,就会查询同区块同收款地址的 ERC-20 Transfer 事件并输出 transferMatch。金额优先读取 web3AmountRawamountRaw;未提供时会按订单金额和 tokenDecimals 换算为最小单位。

Web3 支付结算链路已经按这个顺序落地:先用只读确认判断交易状态,再用转账事件匹配校验金额、收款地址和 Token 合约,最后通过复核日志和显式结算执行器进入幂等结算。单独的确认接口仍然只读,不会直接写订单、流水或链上状态。

地址本与元数据缓存

Web3AddressBook 是配置型地址本,适合记录收款钱包、运营钱包、常用 Token 合约、业务合约等低频变更地址。它不替代用户钱包表,也不做余额、权限或风控判断。

java
@RequiredArgsConstructor
public class TreasuryAddressService {

    private final Web3AddressBook addressBook;

    public String treasuryAddress() {
        return addressBook.getRequired("treasury").getAddress();
    }

}

配置示例:

yaml
spring:
  open:
    web3:
      address-book:
        enabled: true
        entries:
          treasury:
            enabled: true
            chain-name: bsc
            address: 0x0000000000000000000000000000000000000001
            type: wallet
            label: Treasury Wallet
            tags:
              - finance
          usdt:
            enabled: true
            chain-name: bsc
            address: 0x55d398326f99059fF775485246999027B3197955
            type: token
            label: USDT
            tags:
              - stablecoin

Token 元数据缓存通过 spring.open.web3.metadata-cache.* 控制。当前只缓存静态元数据:

  • ERC-20 namesymboldecimalstotalSupply
  • ERC-721 namesymbol
  • ERC-165 / ERC-721 / ERC-1155 标准支持识别结果

余额、授权额度、owner、tokenURI、Transfer 事件和交易状态不缓存,因为这些数据可能频繁变化。

yaml
spring:
  open:
    web3:
      metadata-cache:
        enabled: true
        ttl: 10m
        max-size: 2048

数据库配置可覆盖已存在地址别名的字段,例如:

text
spring.open.web3.address-book.entries.treasury.address=0x...
spring.open.web3.address-book.entries.treasury.label=Main Treasury
spring.open.web3.metadata-cache.ttl=30m
spring.open.web3.metadata-cache.max-size=4096

PaymentCreateRequest.attributes 和支付订单 attributes 都支持直接放置 Web3 字段,也支持嵌套到 web3 对象中。框架内部通过 Web3PaymentAttributes 统一解析这些 key:

语义推荐 key兼容 key
链名称chainNameweb3ChainNameweb3Chainchain
交易 HashtransactionHashweb3TransactionHashtxHashhash
确认数requiredConfirmationsweb3RequiredConfirmationsconfirmations

推荐新代码优先使用 chainNametransactionHashrequiredConfirmations。兼容 key 主要用于接入已有业务字段,后续补偿和结算代码不要再手写这些字符串。

相关

Released under the Apache License 2.0.