如何通过ABI获取User结构体中的数组?已部署合约适配方案
问题解决:从已部署合约的User结构体中获取weeklyBusiness数组
首先明确:直接修改ABI无法解决问题。因为Solidity为public映射自动生成的getter函数,只会返回结构体中的值类型字段(比如你的level和start),不会返回数组类型(不管是动态数组WeeklyBusiness[]还是固定长度数组uint256[3])。你修改ABI只是告诉客户端期望更多返回值,但合约实际的getter函数根本没输出这些数据,所以必然无效。
下面是两种可行的解决方案:
方案一:部署只读辅助合约(Wrapper Contract)
如果你的链上环境允许部署合约,可以写一个只读合约,直接读取原合约的存储数据来获取完整的User结构体信息。
示例代码
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface OriginalContract { struct WeeklyBusiness { uint256 amount; uint256 time; } struct User { uint256 level; uint256 start; WeeklyBusiness[] weeklyBusiness; uint256[3] registered; } mapping(address => User) public userInfo; } contract UserInfoReader { OriginalContract public originalContract; constructor(address _originalContractAddr) { originalContract = OriginalContract(_originalContractAddr); } function getUserInfo(address userAddr) external view returns (OriginalContract.User memory) { return originalContract.userInfo(userAddr); } }
部署这个合约后,调用getUserInfo就能拿到包含所有字段的完整User结构体,包括weeklyBusiness数组和registered数组。
方案二:手动计算存储槽,直接读取链上存储
如果无法部署合约,可以用Web3/Ethers等工具,通过低级调用读取指定存储槽的数据,手动解析出数组内容。
存储位置计算规则
- User结构体的存储槽:对于映射
userInfo(假设它是合约第一个状态变量,存储槽编号为0),用户地址对应的User结构体起始存储槽为:const userStructSlot = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [userAddr, 0])); - 各字段的位置:
level:存储槽userStructSlot,偏移0(直接读取该槽的值)start:存储槽userStructSlot,偏移32(读取该槽的后32字节)weeklyBusiness数组长度:存储槽userStructSlot,偏移64(读取该槽的第三个32字节段)weeklyBusiness数组元素:每个元素的存储槽为ethers.utils.keccak256(userStructSlot + 64)+ 索引(每个WeeklyBusiness是两个uint256,占64字节,所以索引i对应的元素存在两个连续槽:keccak(...) + i*2和keccak(...) + i*2 +1)registered数组:三个元素分别在存储槽userStructSlot的偏移96、128、160位置(直接读取对应偏移的值)
Ethers.js示例代码
const ethers = require("ethers"); const provider = new ethers.providers.JsonRpcProvider("你的RPC地址"); async function getUserFullInfo(contractAddr, userAddr) { // 计算User结构体的起始存储槽 const mappingSlot = 0; // 假设userInfo是第一个状态变量,存储槽为0 const userStructSlot = ethers.utils.keccak256( ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [userAddr, mappingSlot]) ); // 读取User结构体所在的存储槽(包含level、start、weeklyBusiness长度) const structData = await provider.getStorageAt(contractAddr, userStructSlot); // 解析level、start、weeklyBusiness长度 const level = ethers.BigNumber.from(structData.slice(0, 66)); const start = ethers.BigNumber.from(structData.slice(66, 130)); const weeklyBusinessLength = ethers.BigNumber.from(structData.slice(130, 194)); // 读取weeklyBusiness数组元素 const weeklyBusiness = []; const arrayDataSlot = ethers.utils.keccak256(ethers.utils.hexConcat([userStructSlot, "0x40"])); // 0x40是64的十六进制,对应weeklyBusiness在结构体中的偏移 for (let i = 0; i < weeklyBusinessLength.toNumber(); i++) { const amountSlot = ethers.BigNumber.from(arrayDataSlot).add(i * 2).toHexString(); const timeSlot = ethers.BigNumber.from(arrayDataSlot).add(i * 2 + 1).toHexString(); const amount = ethers.BigNumber.from(await provider.getStorageAt(contractAddr, amountSlot)); const time = ethers.BigNumber.from(await provider.getStorageAt(contractAddr, timeSlot)); weeklyBusiness.push({ amount, time }); } // 读取registered数组 const registered = []; const registeredOffset1 = ethers.utils.hexConcat([userStructSlot, "0x60"]); // 0x60是96的十六进制 const registeredOffset2 = ethers.utils.hexConcat([userStructSlot, "0x80"]); // 0x80是128 const registeredOffset3 = ethers.utils.hexConcat([userStructSlot, "0xa0"]); // 0xa0是160 registered.push(ethers.BigNumber.from(await provider.getStorageAt(contractAddr, registeredOffset1))); registered.push(ethers.BigNumber.from(await provider.getStorageAt(contractAddr, registeredOffset2))); registered.push(ethers.BigNumber.from(await provider.getStorageAt(contractAddr, registeredOffset3))); return { level, start, weeklyBusiness, registered }; } // 调用示例 getUserFullInfo("原合约地址", "目标用户地址").then(info => console.log(info));
内容的提问来源于stack exchange,提问作者Suvi Negi
相关产品推荐
相关产品推荐

