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 / safeTransferFrom) | Web3.erc721Write(...) / erc1155Write(...) |
| Token 批量写交易编排(顺序 nonce、失败跳过、可选广播) | Web3.tokenBatch(...) |
| 原始单位 / 显示单位金额换算 | Web3Amounts |
ERC-20 只读查询与 Transfer 事件
Token 常规只读查询通过 Web3TokenClient 完成。它内部仍然复用 Web3Manager.contractCall(...),不会泄露 web3j 类型到业务模块:
@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) | 查询 name、symbol、decimals、totalSupply |
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) 写交易签名,可选广播 |
name 和 symbol 优先按标准 string 读取;如果旧合约返回 bytes32,会自动降级读取并裁剪末尾空字节。erc20Allowance(...) 只读取链上授权额度;需要授权交易时使用 erc20Approve(...),默认只签名不广播。erc20Transfers(...) 基于 eth_getLogs,支持按区块范围、合约地址、fromAddress 和 toAddress 过滤。
常见 Token 事件定义统一放在 Web3TokenEvents:
Web3EventLogQuery query = Web3EventLogQuery.builder()
.address(contractAddress)
.event(Web3TokenEvents.ERC20_TRANSFER)
.build();当前内置 ERC20_TRANSFER、ERC721_TRANSFER、ERC1155_TRANSFER_SINGLE 和 ERC1155_TRANSFER_BATCH。业务扫描器直接复用这些定义,不要重复手写常见 Token 事件 ABI。
生产环境扫描历史 Transfer 时,建议设置 blockRangeSize,让客户端把一个大区块范围拆成多个小的 RPC 请求:
Web3TokenTransferQuery query = Web3TokenTransferQuery.builder()
.contractAddress("0x0000000000000000000000000000000000000002")
.fromBlock("18000000")
.toBlock("18050000")
.blockRangeSize(BigInteger.valueOf(1000))
.build();拆分规则:
blockRangeSize表示单次 RPC 最多查询多少个区块,包含起止区块。fromBlock/toBlock为数字、十六进制数字或latest时可拆分;latest会先解析为当前区块高度。safe、finalized、pending等动态标签不自动拆分,会退回单次eth_getLogs。fromAddress/toAddress等事件过滤地址会先通过EvmAddresses.require(...)校验,避免无效地址或过长地址被错误编码为 topic。- 该策略只减少 RPC 超时和节点限制风险,不保存扫描游标;游标重扫和多实例补偿应放在 Scheduler 或业务模块中实现。
ERC-20 写交易辅助只覆盖 transfer 和 approve,不提供 transferFrom、订单结算或本地资产流水修改。批量 Token/NFT 写交易请使用 batchTransactions(...) 做顺序 nonce 编排;链上支付结算由 Payment / Money Web3 复核结算链路在事务幂等边界内处理。
ERC-721 只读查询
NFT 常规只读查询同样通过 Web3TokenClient 完成,内部仍然复用合约只读调用:
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) | 查询 name 和 symbol |
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 数组参数:
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) 写交易签名,可选广播 |
ownerAddresses 和 tokenIds 必须数量一致;数量不一致会抛出 Web3 业务异常。ERC-1155 写交易辅助只负责 ABI data、签名和可选广播,不负责本地资产归属变更。
NFT Transfer 事件查询
NFT 转移事件也通过 Web3TokenClient 查询,内部复用 eth_getLogs 和 ABI 事件解码。常见用法:
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) 事件,支持 fromAddress、toAddress、tokenId topic 过滤 |
erc1155TransferSingles(chain, query) | 查询 ERC-1155 TransferSingle(address,address,address,uint256,uint256) 事件,支持 operatorAddress、fromAddress、toAddress topic 过滤 |
erc1155TransferBatches(chain, query) | 查询 ERC-1155 TransferBatch(address,address,address,uint256[],uint256[]) 事件,支持 operatorAddress、fromAddress、toAddress topic 过滤 |
NFT Transfer 查询同样支持 blockRangeSize,可将大区块范围拆成多个小范围 RPC 请求。operatorAddress、fromAddress、toAddress 等事件过滤地址会先通过 EvmAddresses.require(...) 校验。ERC-1155 的 id / ids 不是 indexed topic,无法像 ERC-721 tokenId 一样在链节点侧直接过滤,需要业务侧按返回结果再筛选。
ERC-20 写交易辅助
ERC-20 transfer 和 approve 通过 Web3TokenClient 生成 ABI data,再复用 Web3 starter 的签名 / 广播边界。业务模块不需要手写 ABI,也不直接依赖 web3j。
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 > 0;approve允许amount = 0,用于撤销授权。- 默认
broadcast=false,只返回signedTransaction和 ABIdata;显式broadcast=true才会广播链上交易。 nonce、gasPrice、gasLimit和chainId可显式传入;未传时沿用底层 EVM 链客户端的兜底逻辑。- 私钥只作为本次签名请求参数使用,不持久化、不打印、不写日志。
- 该能力只负责构造、签名和可选广播链上交易,不修改支付订单、用户资产或本地流水。
NFT / ERC-1155 写交易辅助
ERC-721 和 ERC-1155 常见写交易同样通过 Web3TokenClient 生成 ABI data,并复用签名 / 广播边界:
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,tokenIds与amounts数量必须一致。 - ERC-1155
data为空时自动使用0x。 - 所有 NFT / ERC-1155 写交易辅助默认只签名不广播,不修改本地订单、资产或流水。
Token 批量写交易编排
如果需要一次生成多笔 Token/NFT 写交易计划,使用 batchTransactions(...)。它不会把多笔转账强行合并成一个合约调用,而是按条目顺序复用单笔写交易能力,统一处理 nonce、签名、可选广播、失败跳过和统计:
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_TRANSFER | ERC-20 transfer(address,uint256) |
ERC20_APPROVE | ERC-20 approve(address,uint256) |
ERC721_APPROVE | ERC-721 approve(address,uint256) |
ERC721_SET_APPROVAL_FOR_ALL | ERC-721 setApprovalForAll(address,bool) |
ERC721_TRANSFER_FROM | ERC-721 transferFrom(address,address,uint256) |
ERC721_SAFE_TRANSFER_FROM | ERC-721 safeTransferFrom |
ERC1155_SET_APPROVAL_FOR_ALL | ERC-1155 setApprovalForAll(address,bool) |
ERC1155_SAFE_TRANSFER_FROM | ERC-1155 safeTransferFrom(address,address,uint256,uint256,bytes) |
ERC1155_SAFE_BATCH_TRANSFER_FROM | ERC-1155 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) |
批量编排规则:
privateKey和fromAddress是批量请求级配置,单条 item 不单独携带私钥。startNonce为空时,第一笔由链客户端按pending查询,后续使用已签名 nonce 顺序递增。- 单条 item 显式配置
nonce时,以单条为准。 - 单条失败不会中断整批,结果会标记为
skipped,用于后台展示和人工复核。 broadcast=false适合后台复核和离线签名计划;broadcast=true才会逐笔广播。- 批量入口只编排链上写交易,不修改本地订单、资产或流水。
Web3 Token 金额换算
Web3 starter 提供 Web3TokenAmounts,用于业务金额和链上原始最小单位之间转换。该工具不访问 RPC,也不处理私钥,只负责精度换算:
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-mismatch | Token 合约不一致 |
amount-mismatch | 原始最小单位金额不一致 |
transfer-query-failed | 查询链上转账事件失败 |
Money Web3 只读确认接口已经接入这个 matcher:只要支付订单 attributes 里能解析到 receiverAddress、tokenContract 和交易区块号,就会查询同区块同收款地址的 ERC-20 Transfer 事件并输出 transferMatch。金额优先读取 web3AmountRaw 或 amountRaw;未提供时会按订单金额和 tokenDecimals 换算为最小单位。
Web3 支付结算链路已经按这个顺序落地:先用只读确认判断交易状态,再用转账事件匹配校验金额、收款地址和 Token 合约,最后通过复核日志和显式结算执行器进入幂等结算。单独的确认接口仍然只读,不会直接写订单、流水或链上状态。
地址本与元数据缓存
Web3AddressBook 是配置型地址本,适合记录收款钱包、运营钱包、常用 Token 合约、业务合约等低频变更地址。它不替代用户钱包表,也不做余额、权限或风控判断。
@RequiredArgsConstructor
public class TreasuryAddressService {
private final Web3AddressBook addressBook;
public String treasuryAddress() {
return addressBook.getRequired("treasury").getAddress();
}
}配置示例:
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:
- stablecoinToken 元数据缓存通过 spring.open.web3.metadata-cache.* 控制。当前只缓存静态元数据:
- ERC-20
name、symbol、decimals、totalSupply - ERC-721
name、symbol - ERC-165 / ERC-721 / ERC-1155 标准支持识别结果
余额、授权额度、owner、tokenURI、Transfer 事件和交易状态不缓存,因为这些数据可能频繁变化。
spring:
open:
web3:
metadata-cache:
enabled: true
ttl: 10m
max-size: 2048数据库配置可覆盖已存在地址别名的字段,例如:
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=4096PaymentCreateRequest.attributes 和支付订单 attributes 都支持直接放置 Web3 字段,也支持嵌套到 web3 对象中。框架内部通过 Web3PaymentAttributes 统一解析这些 key:
| 语义 | 推荐 key | 兼容 key |
|---|---|---|
| 链名称 | chainName | web3ChainName、web3Chain、chain |
| 交易 Hash | transactionHash | web3TransactionHash、txHash、hash |
| 确认数 | requiredConfirmations | web3RequiredConfirmations、confirmations |
推荐新代码优先使用 chainName、transactionHash 和 requiredConfirmations。兼容 key 主要用于接入已有业务字段,后续补偿和结算代码不要再手写这些字符串。
相关
- 链 RPC / 合约只读调用:web3-rpc.md
- 批量转账 / 资金归集:web3-transfer.md
- 事件扫描 / 检查点:web3-event-scan.md
- Web3 顶层概览:web3.md