代码改变世界

智能合约abi的可视化接口文档生成脚本

2025-04-14 10:03  第二个卿老师  阅读(45)  评论(0)    收藏  举报

背景

合约测试一直有个小痛点:合约开发人员每次给的是abi.json文件,而json文件不方便查找对应的合约接口及参数。于是在网上也找到了对应的工具chaintool.,感兴趣的可以自己下载部署。

解决方案

我主要是想生成一个可视化接口文档,于是自己写了一个脚本如下,也放到了自己的github

import json
from datetime import datetime
from web3 import Web3
from pathlib import Path


class ABIAnalyzer:
    def __init__(self, abi_file_path):
        path = Path(abi_file_path)
        self.contract_name = path.stem
        self.abi = self._load_abi(abi_file_path)
        self.type_examples = {
            'uint': 100,
            'int': -50,
            'bool': True,
            'address': Web3.to_checksum_address("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"),
            'bytes': bytes([0x01, 0x02]),
            'string': "example",
            '[]': ["array_element"],
            'tuple': None  # 结构体占位符
        }

    def _load_abi(self, file_path):
        """加载并验证ABI文件"""
        with open(file_path, 'r') as f:
            data = json.load(f)

        if isinstance(data, dict) and "abi" in data:
            return data["abi"]
        elif isinstance(data, list):
            return data
        else:
            raise ValueError("无法解析ABI文件格式")

    def _get_type_example(self, param):
        """
        递归生成参数示例值
        :param param: ABI参数定义(dict结构)
        :return: Python示例值
        """
        solidity_type = param['type']

        # 处理数组类型
        if '[]' in solidity_type:
            base_type = solidity_type.replace('[]', '')
            return [self._get_type_example({'type': base_type, 'components': param.get('components')})]

        if '[' in solidity_type and ']' in solidity_type:
            base_type, size = solidity_type.split('[')
            size = int(size.replace(']', ''))
            return [self._get_type_example({'type': base_type, 'components': param.get('components')})] * size

        # 处理结构体
        if solidity_type == 'tuple' and 'components' in param:
            return {
                comp['name']: self._get_type_example(comp) for comp in param['components']
            }

        # 处理基础类型
        for key in ['uint', 'int', 'address', 'bool', 'string']:
            if solidity_type.startswith(key):
                return self.type_examples[key]

        if solidity_type.startswith('bytes'):
            return self.type_examples['bytes'] if solidity_type == 'bytes' else bytes(int(solidity_type[5:]))

        return "UNKNOWN_TYPE"

    def _get_type_tree(self, param, indent=0):
        """
        生成类型结构树形描述
        :return: (类型名称, 嵌套结构描述)
        """
        solidity_type = param['type']
        components = param.get('components')

        # 解析结构体
        if solidity_type == 'tuple' and components:
            struct_name = param.get('internalType', 'struct').split('.')[-1]
            children = []
            for comp in components:
                child_type, _ = self._get_type_tree(comp, indent + 1)
                children.append(f"{'  ' * indent}↳ {comp['name']}: {child_type}")
            return (struct_name, '\n'.join(children))

        # 解析数组类型
        if '[]' in solidity_type:
            base_type = solidity_type.replace('[]', '')
            child_type, child_desc = self._get_type_tree({'type': base_type, 'components': components}, indent)
            return (f"{child_type}[]", child_desc)

        if '[' in solidity_type and ']' in solidity_type:
            base_type, size = solidity_type.split('[')
            size = size.replace(']', '')
            child_type, child_desc = self._get_type_tree({'type': base_type, 'components': components}, indent)
            return (f"{child_type}[{size}]", child_desc)

        # 基础类型简化显示
        # simple_type = solidity_type.replace("uint256", "()").replace("uint", "int")
        return (solidity_type, None)

    def generate_markdown(self):
        """生成包含函数和事件的完整文档"""
        md = [
            f"# {self.contract_name} 合约文档",
            f"*自动生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n",
            "## 目录\n"
        ]

        # 生成目录
        md += ["### 函数列表", "| 函数名 | 参数 | 状态 |", "|---|---|---|"]
        functions = [item for item in self.abi if item['type'] == 'function']
        for func in functions:
            state = "🛠 修改" if func.get('stateMutability') in ['payable', 'nonpayable'] else "📖 只读"
            md.append(f"| [{func['name']}](#{func['name'].lower()}) | {len(func['inputs'])} | {state} |")

        md += ["\n### 事件列表", "| 事件名 | 参数 | 索引参数 |", "|---|---|---|"]
        events = [item for item in self.abi if item['type'] == 'event']
        for evt in events:
            indexed_count = sum(1 for inp in evt['inputs'] if inp.get('indexed'))
            md.append(f"| [{evt['name']}](#{evt['name'].lower()}) | {len(evt['inputs'])} | {indexed_count} |")
        md.append("\n")

        md += ["### 构造函数", "| 函数名 | 参数 | 状态 |", "|---|---|---|"]
        constructor = [item for item in self.abi if item['type'] == 'constructor']
        for func in constructor:
            state = "🛠 修改" if func.get('stateMutability') in ['payable', 'nonpayable'] else "📖 只读"
            md.append(f"| constructor | {len(func['inputs'])} | {state} |")

        # 生成函数文档
        md.append("## 函数详情\n")
        for func in functions:
            md += self._generate_function_section(func)

        # 生成事件文档
        md.append("## 事件详情\n")
        for evt in events:
            md += self._generate_event_section(evt)

        return '\n'.join(md)

    def _generate_function_section(self, func_item):
        """生成单个函数的文档部分"""
        section = []
        section.append(f"## {func_item['name']}\n")

        # 函数结构
        inputs = []
        for inp in func_item['inputs']:
            inputs.append(f"{inp['type']} {inp['name']}")
        section.append(f"**函数结构**  \n`{func_item['name']}({', '.join(inputs)})`\n")

        # 参数表格
        if func_item['inputs']:
            section.append("### 参数说明\n")
            section.append("| 参数 | 类型 | 示例值 |")
            section.append("|---|---|---|")
            for inp in func_item['inputs']:
                # 递归解析类型结构
                type_name, type_desc = self._get_type_tree(inp)
                example = self._get_type_example(inp)

                # 主参数行
                section.append(f"| **{inp['name']}** | `{type_name}` | `{self._format_example(example)}` |")

                # 结构体展开描述
                if type_desc:
                    for line in type_desc.split('\n'):
                        line = line.split(': ')
                        type = line[-1]
                        example = self._get_type_example({"type": type})
                        section.append(f"| {line[0]} | {type} | {self._format_example(example)} |")
            section.append("\n")

        # 调用示例
        example_args = [self._get_type_example(inp) for inp in func_item['inputs']]
        args_str = ', '.join([repr(a) for a in example_args])
        section += [
            "### 调用示例",
            "```python",
            f"# 只读调用",
            f"contract.functions.{func_item['name']}({args_str}).call()",
            f"\n# 交易调用",
            f"contract.functions.{func_item['name']}({args_str}).transact({{'from': '0x...'}})",
            "```\n",
            "---\n"
        ]

        return section

    def _generate_event_section(self, event_item):
        """生成单个事件的文档部分"""
        section = []
        section.append(f"## {event_item['name']}\n")

        # 事件签名
        inputs = []
        for inp in event_item['inputs']:
            prefix = "indexed " if inp.get('indexed') else ""
            inputs.append(f"{prefix}{inp['type']} {inp['name']}")
        section.append(f"**事件结构**  \n`{event_item['name']}({', '.join(inputs)})`\n")

        # 参数表格
        if event_item['inputs']:
            section.append("### 事件参数\n")
            section.append("| 参数 | 类型 | 索引 | 示例值 |")
            section.append("|---|---|---|---|")
            for inp in event_item['inputs']:
                # 递归解析类型结构
                type_name, type_desc = self._get_type_tree(inp)
                example = self._get_type_example(inp)

                # 主行
                section.append(
                    f"| **{inp['name']}** | `{type_name}` | {'true' if inp.get('indexed') else 'false'} | "
                    f"`{self._format_example(example)}` |"
                )

                # 结构体展开
                if type_desc:
                    for line in type_desc.split('\n'):
                        section.append(f"| {line} | | | |")
            section.append("\n")

        # 事件监听示例
        section.append("### 监听示例\n```python")
        section.append("# 创建事件过滤器")
        section.append(f"event_filter = contract.events.{event_item['name']}.create_filter(fromBlock='latest')")
        section.append("\n# 处理事件日志")
        section.append("for event in event_filter.get_new_entries():")
        # section.append(f"print(f\"{event_item['name']}事件触发: {', '.join([f'{inp['name']}={{event.args.{inp['name']}}}' for inp in event_item['inputs']])}\")")
        section.append("```\n---\n")

        return section

    def _format_example(self, example):
        """格式化示例值为易读字符串"""
        if isinstance(example, dict):
            return '{ ' + ', '.join([f"{k}: {self._format_example(v)}" for k, v in example.items()]) + ' }'
        if isinstance(example, bytes):
            return f"0x{example.hex()}" if example else "0x"
        if isinstance(example, list):
            return '[ ' + ', '.join([self._format_example(e) for e in example]) + ' ]'
        return str(example)

    def save_markdown(self, output_file):
        with open(output_file, 'w', encoding='utf-8') as f:
            f.write(self.generate_markdown())


if __name__ == "__main__":
    project_path = Path(__file__).resolve().parent
    abi_path = project_path / "xxx_abi.json         # 需要解析的abi文件
    analyzer = ABIAnalyzer(abi_path)
    analyzer.save_markdown(project_path / "Contract_Documentation.md")

目录效果:

image

函数详情:

image

事件详情:

image