You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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等工具,通过低级调用读取指定存储槽的数据,手动解析出数组内容。

存储位置计算规则

  1. User结构体的存储槽:对于映射userInfo(假设它是合约第一个状态变量,存储槽编号为0),用户地址对应的User结构体起始存储槽为:
    const userStructSlot = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [userAddr, 0]));
    
  2. 各字段的位置:
    • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 10:05:28