trc-8004-sdk 0.1.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdk/__init__.py +123 -0
- sdk/agent_protocol_client.py +180 -0
- sdk/agent_sdk.py +1549 -0
- sdk/chain_utils.py +278 -0
- sdk/cli.py +549 -0
- sdk/client.py +202 -0
- sdk/contract_adapter.py +489 -0
- sdk/exceptions.py +652 -0
- sdk/retry.py +509 -0
- sdk/signer.py +284 -0
- sdk/utils.py +163 -0
- trc_8004_sdk-0.1.0b1.dist-info/METADATA +411 -0
- trc_8004_sdk-0.1.0b1.dist-info/RECORD +16 -0
- trc_8004_sdk-0.1.0b1.dist-info/WHEEL +5 -0
- trc_8004_sdk-0.1.0b1.dist-info/entry_points.txt +2 -0
- trc_8004_sdk-0.1.0b1.dist-info/top_level.txt +1 -0
sdk/chain_utils.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TRC-8004 SDK 链上工具模块
|
|
3
|
+
|
|
4
|
+
提供区块链数据加载和事件监听的工具函数。
|
|
5
|
+
|
|
6
|
+
Functions:
|
|
7
|
+
normalize_hash: 规范化哈希字符串
|
|
8
|
+
load_request_data: 从 URI 加载请求数据
|
|
9
|
+
fetch_event_logs: 获取合约事件日志
|
|
10
|
+
fetch_trongrid_events: 从 TronGrid API 获取事件
|
|
11
|
+
|
|
12
|
+
Supported URI Schemes:
|
|
13
|
+
- file://: 本地文件
|
|
14
|
+
- ipfs://: IPFS 内容(通过网关)
|
|
15
|
+
- http://, https://: HTTP(S) URL
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> from sdk.chain_utils import load_request_data, normalize_hash
|
|
19
|
+
>>> data = load_request_data("ipfs://QmXxx...")
|
|
20
|
+
>>> hash_str = normalize_hash("0xABC123") # -> "abc123"
|
|
21
|
+
|
|
22
|
+
Environment Variables:
|
|
23
|
+
IPFS_GATEWAY_URL: IPFS 网关地址,默认 https://ipfs.io/ipfs
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
from typing import Optional, Any
|
|
30
|
+
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_hash(value: Optional[str]) -> str:
|
|
35
|
+
"""
|
|
36
|
+
规范化哈希字符串。
|
|
37
|
+
|
|
38
|
+
将哈希转换为小写并移除 0x 前缀。
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
value: 哈希字符串,可带 0x 前缀
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
规范化的哈希字符串(小写,无前缀),
|
|
45
|
+
如果输入为空则返回空字符串
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> normalize_hash("0xABC123")
|
|
49
|
+
'abc123'
|
|
50
|
+
>>> normalize_hash("DEF456")
|
|
51
|
+
'def456'
|
|
52
|
+
>>> normalize_hash(None)
|
|
53
|
+
''
|
|
54
|
+
"""
|
|
55
|
+
if not value:
|
|
56
|
+
return ""
|
|
57
|
+
cleaned = value.lower()
|
|
58
|
+
if cleaned.startswith("0x"):
|
|
59
|
+
cleaned = cleaned[2:]
|
|
60
|
+
return cleaned
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_request_data(request_uri: str) -> str:
|
|
64
|
+
"""
|
|
65
|
+
从 URI 加载请求数据。
|
|
66
|
+
|
|
67
|
+
支持多种 URI 协议:
|
|
68
|
+
- file://: 从本地文件系统读取
|
|
69
|
+
- ipfs://: 通过 IPFS 网关获取
|
|
70
|
+
- http://, https://: 直接 HTTP 请求
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
request_uri: 数据 URI
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
加载的数据内容(字符串)
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
FileNotFoundError: 本地文件不存在
|
|
80
|
+
httpx.HTTPStatusError: HTTP 请求失败
|
|
81
|
+
httpx.TimeoutException: 请求超时
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> # 从本地文件加载
|
|
85
|
+
>>> data = load_request_data("file:///path/to/file.json")
|
|
86
|
+
>>>
|
|
87
|
+
>>> # 从 IPFS 加载
|
|
88
|
+
>>> data = load_request_data("ipfs://QmXxx...")
|
|
89
|
+
>>>
|
|
90
|
+
>>> # 从 HTTP URL 加载
|
|
91
|
+
>>> data = load_request_data("https://example.com/data.json")
|
|
92
|
+
|
|
93
|
+
Note:
|
|
94
|
+
- IPFS 网关可通过 IPFS_GATEWAY_URL 环境变量配置
|
|
95
|
+
- HTTP 请求超时时间为 10 秒
|
|
96
|
+
- 如果 URI 不匹配任何已知协议,原样返回
|
|
97
|
+
"""
|
|
98
|
+
# 本地文件
|
|
99
|
+
if request_uri.startswith("file://"):
|
|
100
|
+
path = request_uri.replace("file://", "", 1)
|
|
101
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
102
|
+
return handle.read()
|
|
103
|
+
|
|
104
|
+
# IPFS 内容
|
|
105
|
+
if request_uri.startswith("ipfs://"):
|
|
106
|
+
cid = request_uri.replace("ipfs://", "", 1)
|
|
107
|
+
gateway = os.getenv("IPFS_GATEWAY_URL", "https://ipfs.io/ipfs")
|
|
108
|
+
url = f"{gateway.rstrip('/')}/{cid}"
|
|
109
|
+
with httpx.Client(timeout=10) as client:
|
|
110
|
+
response = client.get(url)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
return response.text
|
|
113
|
+
|
|
114
|
+
# HTTP(S) URL
|
|
115
|
+
if request_uri.startswith("http://") or request_uri.startswith("https://"):
|
|
116
|
+
with httpx.Client(timeout=10) as client:
|
|
117
|
+
response = client.get(request_uri)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
return response.text
|
|
120
|
+
|
|
121
|
+
# 未知协议,原样返回
|
|
122
|
+
return request_uri
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def fetch_event_logs(
|
|
126
|
+
client: Any,
|
|
127
|
+
contract_address: str,
|
|
128
|
+
event_name: str,
|
|
129
|
+
from_block: int,
|
|
130
|
+
to_block: int,
|
|
131
|
+
rpc_url: Optional[str] = None,
|
|
132
|
+
) -> list[dict]:
|
|
133
|
+
"""
|
|
134
|
+
获取合约事件日志。
|
|
135
|
+
|
|
136
|
+
尝试多种方式获取事件:
|
|
137
|
+
1. 使用合约对象的 events 属性
|
|
138
|
+
2. 使用客户端的 get_event_result 方法
|
|
139
|
+
3. 回退到 TronGrid API
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
client: 区块链客户端对象(如 tronpy.Tron)
|
|
143
|
+
contract_address: 合约地址
|
|
144
|
+
event_name: 事件名称(如 "ValidationRequest")
|
|
145
|
+
from_block: 起始区块号
|
|
146
|
+
to_block: 结束区块号
|
|
147
|
+
rpc_url: RPC URL,用于 TronGrid API 回退
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
事件日志列表,每个事件为字典格式
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> from tronpy import Tron
|
|
154
|
+
>>> client = Tron()
|
|
155
|
+
>>> events = fetch_event_logs(
|
|
156
|
+
... client=client,
|
|
157
|
+
... contract_address="TValidationRegistry...",
|
|
158
|
+
... event_name="ValidationRequest",
|
|
159
|
+
... from_block=1000000,
|
|
160
|
+
... to_block=1001000,
|
|
161
|
+
... )
|
|
162
|
+
>>> for event in events:
|
|
163
|
+
... print(event["transaction_id"])
|
|
164
|
+
|
|
165
|
+
Note:
|
|
166
|
+
- 不同的获取方式返回的事件格式可能略有不同
|
|
167
|
+
- 建议指定 rpc_url 以确保回退机制可用
|
|
168
|
+
"""
|
|
169
|
+
# 方式 1: 使用合约的 events 属性
|
|
170
|
+
contract = client.get_contract(contract_address)
|
|
171
|
+
event = getattr(contract.events, event_name, None)
|
|
172
|
+
if event and hasattr(event, "get_logs"):
|
|
173
|
+
try:
|
|
174
|
+
return event.get_logs(from_block=from_block, to_block=to_block)
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
# 方式 2: 使用客户端的 get_event_result 方法
|
|
179
|
+
if hasattr(client, "get_event_result"):
|
|
180
|
+
try:
|
|
181
|
+
return client.get_event_result(
|
|
182
|
+
contract_address=contract_address,
|
|
183
|
+
event_name=event_name,
|
|
184
|
+
from_block=from_block,
|
|
185
|
+
to_block=to_block,
|
|
186
|
+
only_confirmed=True,
|
|
187
|
+
limit=200,
|
|
188
|
+
)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# 方式 3: 回退到 TronGrid API
|
|
193
|
+
if rpc_url:
|
|
194
|
+
return fetch_trongrid_events(rpc_url, contract_address, event_name, from_block, to_block)
|
|
195
|
+
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def fetch_trongrid_events(
|
|
200
|
+
rpc_url: str,
|
|
201
|
+
contract_address: str,
|
|
202
|
+
event_name: str,
|
|
203
|
+
from_block: int,
|
|
204
|
+
to_block: int,
|
|
205
|
+
) -> list[dict]:
|
|
206
|
+
"""
|
|
207
|
+
从 TronGrid API 获取合约事件。
|
|
208
|
+
|
|
209
|
+
使用 TronGrid 的 REST API 分页获取事件,
|
|
210
|
+
并按区块范围过滤。
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
rpc_url: TronGrid API 基础 URL(如 https://api.trongrid.io)
|
|
214
|
+
contract_address: 合约地址
|
|
215
|
+
event_name: 事件名称
|
|
216
|
+
from_block: 起始区块号(包含)
|
|
217
|
+
to_block: 结束区块号(包含)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
事件日志列表
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
httpx.HTTPStatusError: API 请求失败
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
>>> events = fetch_trongrid_events(
|
|
227
|
+
... rpc_url="https://api.trongrid.io",
|
|
228
|
+
... contract_address="TValidationRegistry...",
|
|
229
|
+
... event_name="ValidationRequest",
|
|
230
|
+
... from_block=1000000,
|
|
231
|
+
... to_block=1001000,
|
|
232
|
+
... )
|
|
233
|
+
|
|
234
|
+
Note:
|
|
235
|
+
- 使用分页获取所有事件(每页最多 200 条)
|
|
236
|
+
- 区块范围过滤在客户端进行
|
|
237
|
+
- 仅返回已确认的事件
|
|
238
|
+
"""
|
|
239
|
+
base = rpc_url.rstrip("/")
|
|
240
|
+
url = f"{base}/v1/contracts/{contract_address}/events"
|
|
241
|
+
params: dict[str, Any] = {
|
|
242
|
+
"event_name": event_name,
|
|
243
|
+
"only_confirmed": "true",
|
|
244
|
+
"limit": 200,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
items: list[dict] = []
|
|
248
|
+
|
|
249
|
+
# 分页获取所有事件
|
|
250
|
+
while True:
|
|
251
|
+
resp = httpx.get(url, params=params, timeout=10)
|
|
252
|
+
resp.raise_for_status()
|
|
253
|
+
payload = resp.json()
|
|
254
|
+
batch = payload.get("data") or []
|
|
255
|
+
items.extend(batch)
|
|
256
|
+
|
|
257
|
+
# 检查是否有下一页
|
|
258
|
+
fingerprint = (payload.get("meta") or {}).get("fingerprint")
|
|
259
|
+
if not fingerprint:
|
|
260
|
+
break
|
|
261
|
+
params["fingerprint"] = fingerprint
|
|
262
|
+
|
|
263
|
+
# 按区块范围过滤
|
|
264
|
+
if from_block or to_block:
|
|
265
|
+
filtered = []
|
|
266
|
+
for item in items:
|
|
267
|
+
block = item.get("block_number")
|
|
268
|
+
if block is None:
|
|
269
|
+
filtered.append(item)
|
|
270
|
+
continue
|
|
271
|
+
if block < from_block:
|
|
272
|
+
continue
|
|
273
|
+
if block > to_block:
|
|
274
|
+
continue
|
|
275
|
+
filtered.append(item)
|
|
276
|
+
return filtered
|
|
277
|
+
|
|
278
|
+
return items
|