智能合约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")