Solidity中是否存在可查看代币持有者地址及对应代币ID的函数?
Solidity中查看代币持有者及对应代币ID的实现方式
Solidity 标准代币合约(比如OpenZeppelin的ERC721/ERC1155)本身没有内置函数直接返回所有持有者地址列表及对应代币ID,因为区块链合约不会默认存储全局持有者集合,但可以通过以下两种方式实现需求:
一、合约内自定义存储(链上方式)
通过在合约中额外维护数据结构,主动跟踪所有持有者及他们的代币ID,适合需要直接从合约读取数据的场景。
核心逻辑:
- 用数组
holders存储所有唯一持有者地址 - 用映射
isHolder标记地址是否已在持有者列表中,避免重复添加 - 用映射
holderTokens存储每个地址对应的代币ID列表 - 在
mint、transfer等修改持仓的关键函数中,同步更新这些数据结构
示例代码(ERC721合约):
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract CustomNFT is ERC721 { // 存储所有持有者地址 address[] public holders; // 标记地址是否已在持有者列表中 mapping(address => bool) public isHolder; // 存储每个地址对应的代币ID列表 mapping(address => uint256[]) public holderTokens; constructor() ERC721("CustomNFT", "CNFT") {} // 铸造代币时更新持有者数据 function mint(address to, uint256 tokenId) public { _mint(to, tokenId); if (!isHolder[to]) { isHolder[to] = true; holders.push(to); } holderTokens[to].push(tokenId); } // 重写转账逻辑,同步更新持有者数据 function _transfer(address from, address to, uint256 tokenId) internal override { super._transfer(from, to, tokenId); // 处理转出地址:移除对应代币ID,若持仓为空则从持有者列表删除 _removeTokenFromHolder(from, tokenId); if (balanceOf(from) == 0) { isHolder[from] = false; _removeAddressFromHolders(from); } // 处理转入地址:若不在列表则添加,再加入代币ID if (!isHolder[to]) { isHolder[to] = true; holders.push(to); } holderTokens[to].push(tokenId); } // 辅助函数:从地址的代币列表中移除指定ID function _removeTokenFromHolder(address holder, uint256 tokenId) private { uint256[] storage tokens = holderTokens[holder]; for (uint256 i = 0; i < tokens.length; i++) { if (tokens[i] == tokenId) { tokens[i] = tokens[tokens.length - 1]; tokens.pop(); break; } } } // 辅助函数:从持有者列表中移除指定地址 function _removeAddressFromHolders(address holder) private { for (uint256 i = 0; i < holders.length; i++) { if (holders[i] == holder) { holders[i] = holders[holders.length - 1]; holders.pop(); break; } } } // 获取所有持有者地址 function getAllHolders() public view returns (address[] memory) { return holders; } // 获取指定地址的所有代币ID function getTokensByHolder(address holder) public view returns (uint256[] memory) { return holderTokens[holder]; } }
调用getAllHolders()可获取所有持有者地址,再逐个调用getTokensByHolder(address)就能得到对应地址的代币ID列表,输出格式可匹配你给出的示例。
二、链下事件监听(链下方式)
如果不想修改合约,可以通过监听代币合约的Transfer事件,在链下收集所有持有者及对应代币ID,适合前端或第三方数据服务使用。
核心逻辑:
- 监听ERC721合约的
Transfer(address from, address to, uint256 tokenId)事件,或ERC1155合约的TransferSingle(address operator, address from, address to, uint256 id, uint256 value)事件 - 维护链下数据库,根据事件记录更新持仓:
- 当
from为零地址时,是铸造代币,给to地址添加对应代币ID - 当
to为零地址时,是销毁代币,从from地址移除对应代币ID - 其他情况是转账,从
from移除代币ID,给to添加代币ID
- 当
这种方式不消耗链上存储资源,数据准确性依赖对事件的完整监听。
内容的提问来源于stack exchange,提问作者JAWKAR FF
相关产品推荐
相关产品推荐

