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/agent_sdk.py
ADDED
|
@@ -0,0 +1,1549 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TRC-8004 Agent SDK
|
|
3
|
+
|
|
4
|
+
提供 Agent 与链上合约交互的统一接口,支持:
|
|
5
|
+
- 身份注册与元数据管理 (IdentityRegistry)
|
|
6
|
+
- 验证请求与响应 (ValidationRegistry)
|
|
7
|
+
- 信誉反馈提交 (ReputationRegistry)
|
|
8
|
+
- 签名构建与验证
|
|
9
|
+
- 请求构建辅助
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from sdk import AgentSDK
|
|
13
|
+
>>> sdk = AgentSDK(
|
|
14
|
+
... private_key="your_hex_private_key",
|
|
15
|
+
... rpc_url="https://nile.trongrid.io",
|
|
16
|
+
... network="tron:nile",
|
|
17
|
+
... identity_registry="TIdentityAddr",
|
|
18
|
+
... validation_registry="TValidationAddr",
|
|
19
|
+
... reputation_registry="TReputationAddr",
|
|
20
|
+
... )
|
|
21
|
+
>>> tx_id = sdk.register_agent(token_uri="https://example.com/agent.json")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
import httpx
|
|
31
|
+
|
|
32
|
+
from .contract_adapter import ContractAdapter, DummyContractAdapter, TronContractAdapter
|
|
33
|
+
from .exceptions import (
|
|
34
|
+
ChainIdResolutionError,
|
|
35
|
+
ConfigurationError,
|
|
36
|
+
InvalidAddressError,
|
|
37
|
+
InvalidPrivateKeyError,
|
|
38
|
+
NetworkError,
|
|
39
|
+
SignerNotAvailableError,
|
|
40
|
+
)
|
|
41
|
+
from .retry import RetryConfig, DEFAULT_RETRY_CONFIG, retry
|
|
42
|
+
from .signer import Signer, SimpleSigner, TronSigner
|
|
43
|
+
from .utils import canonical_json, canonical_json_str, keccak256_hex, keccak256_bytes
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger("trc8004.sdk")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_hex_key(value: str) -> bool:
|
|
49
|
+
"""检查字符串是否为有效的十六进制私钥"""
|
|
50
|
+
if not value:
|
|
51
|
+
return False
|
|
52
|
+
try:
|
|
53
|
+
bytes.fromhex(value)
|
|
54
|
+
return len(value) in (64, 66) # 32 bytes, with or without 0x
|
|
55
|
+
except ValueError:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_hex_string(value: str) -> bool:
|
|
60
|
+
"""检查字符串是否为有效的十六进制字符串"""
|
|
61
|
+
if not value:
|
|
62
|
+
return False
|
|
63
|
+
try:
|
|
64
|
+
bytes.fromhex(value)
|
|
65
|
+
return True
|
|
66
|
+
except ValueError:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class SDKConfig:
|
|
72
|
+
"""
|
|
73
|
+
SDK 配置类
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
rpc_url: 区块链 RPC 节点地址
|
|
77
|
+
network: 网络标识 (如 "tron:nile", "tron:mainnet", "evm:1")
|
|
78
|
+
timeout: HTTP 请求超时时间(秒)
|
|
79
|
+
identity_registry: IdentityRegistry 合约地址
|
|
80
|
+
validation_registry: ValidationRegistry 合约地址
|
|
81
|
+
reputation_registry: ReputationRegistry 合约地址
|
|
82
|
+
retry_config: 重试配置
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
rpc_url: str = "https://nile.trongrid.io"
|
|
86
|
+
network: str = "tron:nile"
|
|
87
|
+
timeout: int = 10
|
|
88
|
+
identity_registry: Optional[str] = None
|
|
89
|
+
validation_registry: Optional[str] = None
|
|
90
|
+
reputation_registry: Optional[str] = None
|
|
91
|
+
retry_config: RetryConfig = field(default_factory=lambda: DEFAULT_RETRY_CONFIG)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class AgentSDK:
|
|
95
|
+
"""
|
|
96
|
+
TRC-8004 Agent SDK 主类
|
|
97
|
+
|
|
98
|
+
提供与链上合约交互的统一接口,包括:
|
|
99
|
+
- 身份注册与元数据管理
|
|
100
|
+
- 验证请求与响应
|
|
101
|
+
- 信誉反馈提交
|
|
102
|
+
- 签名构建
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
private_key: 私钥(十六进制字符串,可带 0x 前缀)
|
|
106
|
+
rpc_url: RPC 节点地址
|
|
107
|
+
network: 网络标识(如 "tron:nile")
|
|
108
|
+
identity_registry: IdentityRegistry 合约地址
|
|
109
|
+
validation_registry: ValidationRegistry 合约地址
|
|
110
|
+
reputation_registry: ReputationRegistry 合约地址
|
|
111
|
+
fee_limit: 交易费用上限(TRON 特有)
|
|
112
|
+
signer: 自定义签名器(可选)
|
|
113
|
+
contract_adapter: 自定义合约适配器(可选)
|
|
114
|
+
retry_config: 重试配置(可选)
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
InvalidPrivateKeyError: 私钥格式无效
|
|
118
|
+
ConfigurationError: 配置错误
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> sdk = AgentSDK(
|
|
122
|
+
... private_key="your_private_key",
|
|
123
|
+
... rpc_url="https://nile.trongrid.io",
|
|
124
|
+
... network="tron:nile",
|
|
125
|
+
... )
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
private_key: Optional[str] = None,
|
|
131
|
+
rpc_url: Optional[str] = None,
|
|
132
|
+
network: Optional[str] = None,
|
|
133
|
+
identity_registry: Optional[str] = None,
|
|
134
|
+
validation_registry: Optional[str] = None,
|
|
135
|
+
reputation_registry: Optional[str] = None,
|
|
136
|
+
identity_registry_abi_path: Optional[str] = None,
|
|
137
|
+
validation_registry_abi_path: Optional[str] = None,
|
|
138
|
+
reputation_registry_abi_path: Optional[str] = None,
|
|
139
|
+
fee_limit: Optional[int] = None,
|
|
140
|
+
signer: Optional[Signer] = None,
|
|
141
|
+
contract_adapter: Optional[ContractAdapter] = None,
|
|
142
|
+
retry_config: Optional[RetryConfig] = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
# 初始化配置
|
|
145
|
+
self.config = SDKConfig()
|
|
146
|
+
if rpc_url is not None:
|
|
147
|
+
self.config.rpc_url = rpc_url
|
|
148
|
+
if network is not None:
|
|
149
|
+
self.config.network = network
|
|
150
|
+
if identity_registry is not None:
|
|
151
|
+
self.config.identity_registry = identity_registry
|
|
152
|
+
if validation_registry is not None:
|
|
153
|
+
self.config.validation_registry = validation_registry
|
|
154
|
+
if reputation_registry is not None:
|
|
155
|
+
self.config.reputation_registry = reputation_registry
|
|
156
|
+
if retry_config is not None:
|
|
157
|
+
self.config.retry_config = retry_config
|
|
158
|
+
|
|
159
|
+
# 初始化签名器
|
|
160
|
+
if signer is None:
|
|
161
|
+
signer = self._create_signer(private_key)
|
|
162
|
+
self.signer = signer
|
|
163
|
+
|
|
164
|
+
# 初始化合约适配器
|
|
165
|
+
if contract_adapter is None:
|
|
166
|
+
contract_adapter = self._create_contract_adapter(
|
|
167
|
+
identity_registry_abi_path,
|
|
168
|
+
validation_registry_abi_path,
|
|
169
|
+
reputation_registry_abi_path,
|
|
170
|
+
fee_limit,
|
|
171
|
+
)
|
|
172
|
+
self.contract_adapter = contract_adapter
|
|
173
|
+
|
|
174
|
+
logger.info(
|
|
175
|
+
"SDK initialized: network=%s, rpc=%s, signer=%s",
|
|
176
|
+
self.config.network,
|
|
177
|
+
self.config.rpc_url,
|
|
178
|
+
type(self.signer).__name__,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def address(self) -> Optional[str]:
|
|
183
|
+
"""
|
|
184
|
+
获取签名器的地址
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
签名器地址,如果没有签名器则返回 None
|
|
188
|
+
"""
|
|
189
|
+
if self.signer is None:
|
|
190
|
+
return None
|
|
191
|
+
return self.signer.get_address()
|
|
192
|
+
|
|
193
|
+
def _create_signer(self, private_key: Optional[str]) -> Signer:
|
|
194
|
+
"""创建签名器"""
|
|
195
|
+
if self.config.network.startswith("tron") and private_key:
|
|
196
|
+
cleaned_key = private_key.replace("0x", "")
|
|
197
|
+
if _is_hex_key(cleaned_key):
|
|
198
|
+
try:
|
|
199
|
+
return TronSigner(private_key=cleaned_key)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
raise InvalidPrivateKeyError(str(e)) from e
|
|
202
|
+
else:
|
|
203
|
+
logger.warning("Private key is not hex format, using SimpleSigner")
|
|
204
|
+
return SimpleSigner(private_key=private_key)
|
|
205
|
+
return SimpleSigner(private_key=private_key)
|
|
206
|
+
|
|
207
|
+
def _create_contract_adapter(
|
|
208
|
+
self,
|
|
209
|
+
identity_abi_path: Optional[str],
|
|
210
|
+
validation_abi_path: Optional[str],
|
|
211
|
+
reputation_abi_path: Optional[str],
|
|
212
|
+
fee_limit: Optional[int],
|
|
213
|
+
) -> ContractAdapter:
|
|
214
|
+
"""创建合约适配器"""
|
|
215
|
+
if self.config.network.startswith("tron"):
|
|
216
|
+
return TronContractAdapter(
|
|
217
|
+
rpc_url=self.config.rpc_url,
|
|
218
|
+
identity_registry=self.config.identity_registry,
|
|
219
|
+
validation_registry=self.config.validation_registry,
|
|
220
|
+
reputation_registry=self.config.reputation_registry,
|
|
221
|
+
identity_registry_abi_path=identity_abi_path,
|
|
222
|
+
validation_registry_abi_path=validation_abi_path,
|
|
223
|
+
reputation_registry_abi_path=reputation_abi_path,
|
|
224
|
+
fee_limit=fee_limit,
|
|
225
|
+
retry_config=self.config.retry_config,
|
|
226
|
+
)
|
|
227
|
+
return DummyContractAdapter()
|
|
228
|
+
|
|
229
|
+
def validation_request(
|
|
230
|
+
self,
|
|
231
|
+
validator_addr: str,
|
|
232
|
+
agent_id: int,
|
|
233
|
+
request_uri: str,
|
|
234
|
+
request_hash: Optional[str] = None,
|
|
235
|
+
signer: Optional[Signer] = None,
|
|
236
|
+
) -> str:
|
|
237
|
+
"""
|
|
238
|
+
发起验证请求
|
|
239
|
+
|
|
240
|
+
将执行结果提交到 ValidationRegistry,请求验证者进行验证。
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
validator_addr: 验证者地址
|
|
244
|
+
agent_id: Agent ID(IdentityRegistry 中的 token ID)
|
|
245
|
+
request_uri: 请求数据 URI(如 ipfs://Qm...)
|
|
246
|
+
request_hash: 请求数据哈希(32 bytes,可选,会自动补零)
|
|
247
|
+
signer: 自定义签名器(可选)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
交易 ID
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ContractCallError: 合约调用失败
|
|
254
|
+
SignerNotAvailableError: 签名器不可用
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
>>> tx_id = sdk.validation_request(
|
|
258
|
+
... validator_addr="TValidator...",
|
|
259
|
+
... agent_id=1,
|
|
260
|
+
... request_uri="ipfs://QmXxx",
|
|
261
|
+
... request_hash="0x" + "aa" * 32,
|
|
262
|
+
... )
|
|
263
|
+
"""
|
|
264
|
+
signer = signer or self.signer
|
|
265
|
+
if signer is None:
|
|
266
|
+
raise SignerNotAvailableError()
|
|
267
|
+
|
|
268
|
+
params = [validator_addr, agent_id, request_uri, self._normalize_bytes32(request_hash)]
|
|
269
|
+
logger.debug("validation_request: validator=%s, agent_id=%d", validator_addr, agent_id)
|
|
270
|
+
return self.contract_adapter.send("validation", "validationRequest", params, signer)
|
|
271
|
+
|
|
272
|
+
def validation_response(
|
|
273
|
+
self,
|
|
274
|
+
request_hash: str,
|
|
275
|
+
response: int,
|
|
276
|
+
response_uri: str = "",
|
|
277
|
+
response_hash: Optional[str] = None,
|
|
278
|
+
tag: str = "",
|
|
279
|
+
signer: Optional[Signer] = None,
|
|
280
|
+
) -> str:
|
|
281
|
+
"""
|
|
282
|
+
提交验证响应 (Jan 2026 Update)
|
|
283
|
+
|
|
284
|
+
验证者调用此方法提交验证结果。
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
request_hash: 验证请求哈希(32 bytes)
|
|
288
|
+
response: 验证评分(0-100)
|
|
289
|
+
response_uri: 响应数据 URI(可选)
|
|
290
|
+
response_hash: 响应数据哈希(可选)
|
|
291
|
+
tag: 标签(可选,字符串)
|
|
292
|
+
signer: 自定义签名器(可选)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
交易 ID
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
ContractCallError: 合约调用失败
|
|
299
|
+
"""
|
|
300
|
+
signer = signer or self.signer
|
|
301
|
+
if signer is None:
|
|
302
|
+
raise SignerNotAvailableError()
|
|
303
|
+
|
|
304
|
+
params = [
|
|
305
|
+
self._normalize_bytes32(request_hash),
|
|
306
|
+
response,
|
|
307
|
+
response_uri,
|
|
308
|
+
self._normalize_bytes32(response_hash),
|
|
309
|
+
tag,
|
|
310
|
+
]
|
|
311
|
+
logger.debug("validation_response: request_hash=%s, response=%d", request_hash[:18], response)
|
|
312
|
+
return self.contract_adapter.send("validation", "validationResponse", params, signer)
|
|
313
|
+
|
|
314
|
+
def submit_reputation(
|
|
315
|
+
self,
|
|
316
|
+
agent_id: int,
|
|
317
|
+
score: int,
|
|
318
|
+
tag1: str = "",
|
|
319
|
+
tag2: str = "",
|
|
320
|
+
endpoint: str = "",
|
|
321
|
+
feedback_uri: str = "",
|
|
322
|
+
feedback_hash: Optional[str] = None,
|
|
323
|
+
signer: Optional[Signer] = None,
|
|
324
|
+
) -> str:
|
|
325
|
+
"""
|
|
326
|
+
提交信誉反馈 (Jan 2026 Update)
|
|
327
|
+
|
|
328
|
+
向 ReputationRegistry 提交对 Agent 的评分反馈。
|
|
329
|
+
|
|
330
|
+
注意:Jan 2026 更新移除了 feedbackAuth 预授权机制,现在任何人都可以直接提交反馈。
|
|
331
|
+
Spam/Sybil 防护通过链下过滤和信誉系统处理。
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
agent_id: Agent ID
|
|
335
|
+
score: 评分(0-100)
|
|
336
|
+
tag1: 标签1(可选,字符串)
|
|
337
|
+
tag2: 标签2(可选,字符串)
|
|
338
|
+
endpoint: 使用的 endpoint(可选)
|
|
339
|
+
feedback_uri: 反馈文件 URI(可选)
|
|
340
|
+
feedback_hash: 反馈文件哈希(可选,IPFS 不需要)
|
|
341
|
+
signer: 自定义签名器(可选)
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
交易 ID
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
ContractCallError: 合约调用失败
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
>>> tx_id = sdk.submit_reputation(
|
|
351
|
+
... agent_id=1,
|
|
352
|
+
... score=95,
|
|
353
|
+
... tag1="execution",
|
|
354
|
+
... tag2="market-swap",
|
|
355
|
+
... endpoint="/a2a/x402/execute",
|
|
356
|
+
... )
|
|
357
|
+
"""
|
|
358
|
+
signer = signer or self.signer
|
|
359
|
+
if signer is None:
|
|
360
|
+
raise SignerNotAvailableError()
|
|
361
|
+
|
|
362
|
+
params = [
|
|
363
|
+
agent_id,
|
|
364
|
+
score,
|
|
365
|
+
tag1,
|
|
366
|
+
tag2,
|
|
367
|
+
endpoint,
|
|
368
|
+
feedback_uri,
|
|
369
|
+
self._normalize_bytes32(feedback_hash),
|
|
370
|
+
]
|
|
371
|
+
logger.debug("submit_reputation: agent_id=%d, score=%d", agent_id, score)
|
|
372
|
+
return self.contract_adapter.send("reputation", "giveFeedback", params, signer)
|
|
373
|
+
|
|
374
|
+
def register_agent(
|
|
375
|
+
self,
|
|
376
|
+
token_uri: Optional[str] = None,
|
|
377
|
+
metadata: Optional[list[dict]] = None,
|
|
378
|
+
signer: Optional[Signer] = None,
|
|
379
|
+
) -> str:
|
|
380
|
+
"""
|
|
381
|
+
注册 Agent
|
|
382
|
+
|
|
383
|
+
在 IdentityRegistry 中注册新的 Agent,获得唯一的 Agent ID。
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
token_uri: Agent 元数据 URI(如 https://example.com/agent.json)
|
|
387
|
+
metadata: 初始元数据列表,格式为 [{"key": "name", "value": "MyAgent"}, ...]
|
|
388
|
+
signer: 自定义签名器(可选)
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
交易 ID
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
ContractCallError: 合约调用失败
|
|
395
|
+
|
|
396
|
+
Example:
|
|
397
|
+
>>> tx_id = sdk.register_agent(
|
|
398
|
+
... token_uri="https://example.com/agent.json",
|
|
399
|
+
... metadata=[{"key": "name", "value": "MyAgent"}],
|
|
400
|
+
... )
|
|
401
|
+
"""
|
|
402
|
+
signer = signer or self.signer
|
|
403
|
+
if signer is None:
|
|
404
|
+
raise SignerNotAvailableError()
|
|
405
|
+
|
|
406
|
+
token_uri = token_uri or ""
|
|
407
|
+
if metadata is not None:
|
|
408
|
+
normalized = self._normalize_metadata_entries(metadata)
|
|
409
|
+
params = [token_uri, normalized]
|
|
410
|
+
logger.debug("register_agent: uri=%s, metadata_count=%d", token_uri, len(normalized))
|
|
411
|
+
return self.contract_adapter.send("identity", "register", params, signer)
|
|
412
|
+
|
|
413
|
+
if token_uri:
|
|
414
|
+
params = [token_uri]
|
|
415
|
+
else:
|
|
416
|
+
params = []
|
|
417
|
+
logger.debug("register_agent: uri=%s", token_uri or "(empty)")
|
|
418
|
+
return self.contract_adapter.send("identity", "register", params, signer)
|
|
419
|
+
|
|
420
|
+
@staticmethod
|
|
421
|
+
def extract_metadata_from_card(card: dict) -> list[dict]:
|
|
422
|
+
"""
|
|
423
|
+
从 agent-card.json 提取关键信息作为链上 metadata。
|
|
424
|
+
|
|
425
|
+
注意:根据 ERC-8004 规范,链上 metadata 应该是最小化的。
|
|
426
|
+
大部分信息应该存储在 token_uri 指向的 registration file 中。
|
|
427
|
+
|
|
428
|
+
此方法只提取真正需要链上可组合性的字段:
|
|
429
|
+
- name: Agent 名称(便于链上查询)
|
|
430
|
+
- version: 版本号
|
|
431
|
+
|
|
432
|
+
其他信息(description, skills, endpoints, tags 等)应通过 token_uri 获取。
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
card: agent-card.json 内容
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
metadata 列表,格式为 [{"key": "name", "value": "MyAgent"}, ...]
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
>>> with open("agent-card.json") as f:
|
|
442
|
+
... card = json.load(f)
|
|
443
|
+
>>> metadata = AgentSDK.extract_metadata_from_card(card)
|
|
444
|
+
>>> tx_id = sdk.register_agent(token_uri="https://...", metadata=metadata)
|
|
445
|
+
"""
|
|
446
|
+
metadata = []
|
|
447
|
+
|
|
448
|
+
# 只提取最关键的字段用于链上查询
|
|
449
|
+
if card.get("name"):
|
|
450
|
+
metadata.append({"key": "name", "value": card["name"]})
|
|
451
|
+
if card.get("version"):
|
|
452
|
+
metadata.append({"key": "version", "value": card["version"]})
|
|
453
|
+
|
|
454
|
+
return metadata
|
|
455
|
+
|
|
456
|
+
@staticmethod
|
|
457
|
+
def extract_full_metadata_from_card(card: dict) -> list[dict]:
|
|
458
|
+
"""
|
|
459
|
+
从 agent-card.json 提取完整信息作为链上 metadata。
|
|
460
|
+
|
|
461
|
+
警告:这会将大量数据写入链上,增加 gas 成本。
|
|
462
|
+
通常不推荐使用,除非有特殊的链上可组合性需求。
|
|
463
|
+
|
|
464
|
+
根据 ERC-8004 规范,建议使用 token_uri 指向链下 registration file。
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
card: agent-card.json 内容
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
metadata 列表
|
|
471
|
+
"""
|
|
472
|
+
import json as json_module
|
|
473
|
+
metadata = []
|
|
474
|
+
|
|
475
|
+
# 基础字段
|
|
476
|
+
if card.get("name"):
|
|
477
|
+
metadata.append({"key": "name", "value": card["name"]})
|
|
478
|
+
if card.get("description"):
|
|
479
|
+
metadata.append({"key": "description", "value": card["description"]})
|
|
480
|
+
if card.get("version"):
|
|
481
|
+
metadata.append({"key": "version", "value": card["version"]})
|
|
482
|
+
if card.get("url"):
|
|
483
|
+
metadata.append({"key": "url", "value": card["url"]})
|
|
484
|
+
|
|
485
|
+
# 复杂字段 (JSON 序列化)
|
|
486
|
+
if card.get("skills"):
|
|
487
|
+
skills_summary = [{"id": s.get("id"), "name": s.get("name")} for s in card["skills"]]
|
|
488
|
+
metadata.append({"key": "skills", "value": json_module.dumps(skills_summary, ensure_ascii=False)})
|
|
489
|
+
|
|
490
|
+
if card.get("tags"):
|
|
491
|
+
metadata.append({"key": "tags", "value": json_module.dumps(card["tags"], ensure_ascii=False)})
|
|
492
|
+
|
|
493
|
+
if card.get("endpoints"):
|
|
494
|
+
endpoints_summary = [{"name": e.get("name"), "endpoint": e.get("endpoint")} for e in card["endpoints"]]
|
|
495
|
+
metadata.append({"key": "endpoints", "value": json_module.dumps(endpoints_summary, ensure_ascii=False)})
|
|
496
|
+
|
|
497
|
+
if card.get("capabilities"):
|
|
498
|
+
metadata.append({"key": "capabilities", "value": json_module.dumps(card["capabilities"], ensure_ascii=False)})
|
|
499
|
+
|
|
500
|
+
return metadata
|
|
501
|
+
|
|
502
|
+
def update_metadata(
|
|
503
|
+
self,
|
|
504
|
+
agent_id: int,
|
|
505
|
+
key: str,
|
|
506
|
+
value: str | bytes,
|
|
507
|
+
signer: Optional[Signer] = None,
|
|
508
|
+
) -> str:
|
|
509
|
+
"""
|
|
510
|
+
更新 Agent 元数据
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
agent_id: Agent ID
|
|
514
|
+
key: 元数据键
|
|
515
|
+
value: 元数据值(字符串或字节)
|
|
516
|
+
signer: 自定义签名器(可选)
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
交易 ID
|
|
520
|
+
|
|
521
|
+
Raises:
|
|
522
|
+
ContractCallError: 合约调用失败
|
|
523
|
+
"""
|
|
524
|
+
signer = signer or self.signer
|
|
525
|
+
if signer is None:
|
|
526
|
+
raise SignerNotAvailableError()
|
|
527
|
+
|
|
528
|
+
if isinstance(value, str):
|
|
529
|
+
value = value.encode("utf-8")
|
|
530
|
+
params = [agent_id, key, value]
|
|
531
|
+
logger.debug("update_metadata: agent_id=%d, key=%s", agent_id, key)
|
|
532
|
+
return self.contract_adapter.send("identity", "setMetadata", params, signer)
|
|
533
|
+
|
|
534
|
+
def set_agent_wallet(
|
|
535
|
+
self,
|
|
536
|
+
agent_id: int,
|
|
537
|
+
wallet_address: str,
|
|
538
|
+
deadline: int,
|
|
539
|
+
wallet_signer: Optional[Signer] = None,
|
|
540
|
+
signer: Optional[Signer] = None,
|
|
541
|
+
) -> str:
|
|
542
|
+
"""
|
|
543
|
+
设置 Agent 钱包地址(需要 EIP-712 签名验证)(Jan 2026 Update)
|
|
544
|
+
|
|
545
|
+
根据 ERC-8004 规范,agentWallet 是保留字段,设置时需要证明调用者控制该钱包。
|
|
546
|
+
此方法会自动生成 EIP-712 格式的钱包所有权证明签名。
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
agent_id: Agent ID
|
|
550
|
+
wallet_address: 要设置的钱包地址
|
|
551
|
+
deadline: 签名过期时间(Unix 时间戳)
|
|
552
|
+
wallet_signer: 钱包签名器(用于生成所有权证明,默认使用 self.signer)
|
|
553
|
+
signer: 交易签名器(Agent owner,默认使用 self.signer)
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
交易 ID
|
|
557
|
+
|
|
558
|
+
Raises:
|
|
559
|
+
ContractCallError: 合约调用失败
|
|
560
|
+
SignerNotAvailableError: 签名器不可用
|
|
561
|
+
|
|
562
|
+
Example:
|
|
563
|
+
>>> import time
|
|
564
|
+
>>> deadline = int(time.time()) + 3600 # 1 hour from now
|
|
565
|
+
>>>
|
|
566
|
+
>>> # 设置自己的钱包(signer 同时是 owner 和 wallet)
|
|
567
|
+
>>> tx_id = sdk.set_agent_wallet(
|
|
568
|
+
... agent_id=1,
|
|
569
|
+
... wallet_address="TWallet...",
|
|
570
|
+
... deadline=deadline,
|
|
571
|
+
... )
|
|
572
|
+
>>>
|
|
573
|
+
>>> # 设置其他钱包(需要该钱包的签名器)
|
|
574
|
+
>>> wallet_signer = TronSigner(private_key="wallet_private_key")
|
|
575
|
+
>>> tx_id = sdk.set_agent_wallet(
|
|
576
|
+
... agent_id=1,
|
|
577
|
+
... wallet_address="TWallet...",
|
|
578
|
+
... deadline=deadline,
|
|
579
|
+
... wallet_signer=wallet_signer,
|
|
580
|
+
... )
|
|
581
|
+
"""
|
|
582
|
+
signer = signer or self.signer
|
|
583
|
+
if signer is None:
|
|
584
|
+
raise SignerNotAvailableError()
|
|
585
|
+
|
|
586
|
+
wallet_signer = wallet_signer or self.signer
|
|
587
|
+
if wallet_signer is None:
|
|
588
|
+
raise SignerNotAvailableError("Wallet signer required for ownership proof")
|
|
589
|
+
|
|
590
|
+
# 构建 EIP-712 钱包所有权证明签名
|
|
591
|
+
signature = self._build_eip712_wallet_signature(
|
|
592
|
+
agent_id=agent_id,
|
|
593
|
+
wallet_address=wallet_address,
|
|
594
|
+
deadline=deadline,
|
|
595
|
+
wallet_signer=wallet_signer,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
params = [agent_id, wallet_address, deadline, signature]
|
|
599
|
+
logger.debug("set_agent_wallet: agent_id=%d, wallet=%s, deadline=%d", agent_id, wallet_address[:12], deadline)
|
|
600
|
+
return self.contract_adapter.send("identity", "setAgentWallet", params, signer)
|
|
601
|
+
|
|
602
|
+
def _build_eip712_wallet_signature(
|
|
603
|
+
self,
|
|
604
|
+
agent_id: int,
|
|
605
|
+
wallet_address: str,
|
|
606
|
+
deadline: int,
|
|
607
|
+
wallet_signer: Signer,
|
|
608
|
+
) -> bytes:
|
|
609
|
+
"""
|
|
610
|
+
构建 EIP-712 钱包所有权证明签名 (Jan 2026 Update)
|
|
611
|
+
|
|
612
|
+
EIP-712 Domain:
|
|
613
|
+
name: "ERC-8004 IdentityRegistry"
|
|
614
|
+
version: "1.1"
|
|
615
|
+
chainId: <chain_id>
|
|
616
|
+
verifyingContract: <identity_registry>
|
|
617
|
+
|
|
618
|
+
TypeHash: SetAgentWallet(uint256 agentId,address newWallet,uint256 deadline)
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
agent_id: Agent ID
|
|
622
|
+
wallet_address: 钱包地址
|
|
623
|
+
deadline: 签名过期时间
|
|
624
|
+
wallet_signer: 钱包签名器
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
签名字节
|
|
628
|
+
"""
|
|
629
|
+
chain_id = self.resolve_chain_id()
|
|
630
|
+
if chain_id is None:
|
|
631
|
+
# 默认使用 TRON Nile testnet chain ID
|
|
632
|
+
chain_id = 3448148188
|
|
633
|
+
logger.warning("Could not resolve chain ID, using default: %d", chain_id)
|
|
634
|
+
|
|
635
|
+
identity_registry = self.config.identity_registry or ""
|
|
636
|
+
|
|
637
|
+
# EIP-712 Domain Separator
|
|
638
|
+
domain_type_hash = keccak256_bytes(
|
|
639
|
+
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
640
|
+
)
|
|
641
|
+
domain_separator = keccak256_bytes(b"".join([
|
|
642
|
+
domain_type_hash,
|
|
643
|
+
keccak256_bytes(b"ERC-8004 IdentityRegistry"),
|
|
644
|
+
keccak256_bytes(b"1.1"),
|
|
645
|
+
self._abi_encode_uint(chain_id),
|
|
646
|
+
self._abi_encode_address(identity_registry),
|
|
647
|
+
]))
|
|
648
|
+
|
|
649
|
+
# SetAgentWallet struct hash
|
|
650
|
+
set_agent_wallet_typehash = keccak256_bytes(
|
|
651
|
+
b"SetAgentWallet(uint256 agentId,address newWallet,uint256 deadline)"
|
|
652
|
+
)
|
|
653
|
+
struct_hash = keccak256_bytes(b"".join([
|
|
654
|
+
set_agent_wallet_typehash,
|
|
655
|
+
self._abi_encode_uint(agent_id),
|
|
656
|
+
self._abi_encode_address(wallet_address),
|
|
657
|
+
self._abi_encode_uint(deadline),
|
|
658
|
+
]))
|
|
659
|
+
|
|
660
|
+
# EIP-712 digest
|
|
661
|
+
digest = keccak256_bytes(
|
|
662
|
+
b"\x19\x01" + domain_separator + struct_hash
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Sign the digest
|
|
666
|
+
signature = self._normalize_bytes(wallet_signer.sign_message(digest))
|
|
667
|
+
|
|
668
|
+
# 规范化签名(处理 v 值)
|
|
669
|
+
if len(signature) == 65:
|
|
670
|
+
v = signature[-1]
|
|
671
|
+
if v in (0, 1):
|
|
672
|
+
v += 27
|
|
673
|
+
signature = signature[:64] + bytes([v])
|
|
674
|
+
|
|
675
|
+
return signature
|
|
676
|
+
|
|
677
|
+
def set_agent_uri(
|
|
678
|
+
self,
|
|
679
|
+
agent_id: int,
|
|
680
|
+
new_uri: str,
|
|
681
|
+
signer: Optional[Signer] = None,
|
|
682
|
+
) -> str:
|
|
683
|
+
"""
|
|
684
|
+
更新 Agent 的 URI (Jan 2026 Update)
|
|
685
|
+
|
|
686
|
+
更新 Agent 的 registration file URI。只有 owner 或 approved operator 可以调用。
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
agent_id: Agent ID
|
|
690
|
+
new_uri: 新的 URI
|
|
691
|
+
signer: 自定义签名器(可选)
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
交易 ID
|
|
695
|
+
|
|
696
|
+
Raises:
|
|
697
|
+
ContractCallError: 合约调用失败
|
|
698
|
+
SignerNotAvailableError: 签名器不可用
|
|
699
|
+
|
|
700
|
+
Example:
|
|
701
|
+
>>> tx_id = sdk.set_agent_uri(
|
|
702
|
+
... agent_id=1,
|
|
703
|
+
... new_uri="https://example.com/new-agent.json",
|
|
704
|
+
... )
|
|
705
|
+
"""
|
|
706
|
+
signer = signer or self.signer
|
|
707
|
+
if signer is None:
|
|
708
|
+
raise SignerNotAvailableError()
|
|
709
|
+
|
|
710
|
+
params = [agent_id, new_uri]
|
|
711
|
+
logger.debug("set_agent_uri: agent_id=%d, uri=%s", agent_id, new_uri[:50])
|
|
712
|
+
return self.contract_adapter.send("identity", "setAgentURI", params, signer)
|
|
713
|
+
|
|
714
|
+
# ==================== Identity Registry 只读方法 ====================
|
|
715
|
+
|
|
716
|
+
def get_agent_uri(self, agent_id: int) -> str:
|
|
717
|
+
"""
|
|
718
|
+
获取 Agent 的 tokenURI
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
agent_id: Agent ID
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Agent 的 tokenURI(指向 registration file)
|
|
725
|
+
|
|
726
|
+
Example:
|
|
727
|
+
>>> uri = sdk.get_agent_uri(1)
|
|
728
|
+
>>> print(uri) # "https://example.com/agent.json"
|
|
729
|
+
"""
|
|
730
|
+
params = [agent_id]
|
|
731
|
+
return self.contract_adapter.call("identity", "tokenURI", params)
|
|
732
|
+
|
|
733
|
+
def get_metadata(self, agent_id: int, key: str) -> bytes:
|
|
734
|
+
"""
|
|
735
|
+
获取 Agent 的链上 metadata
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
agent_id: Agent ID
|
|
739
|
+
key: metadata 键名
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
metadata 值(bytes)
|
|
743
|
+
|
|
744
|
+
Example:
|
|
745
|
+
>>> name = sdk.get_metadata(1, "name")
|
|
746
|
+
>>> print(name.decode("utf-8")) # "MyAgent"
|
|
747
|
+
"""
|
|
748
|
+
params = [agent_id, key]
|
|
749
|
+
return self.contract_adapter.call("identity", "getMetadata", params)
|
|
750
|
+
|
|
751
|
+
def agent_exists(self, agent_id: int) -> bool:
|
|
752
|
+
"""
|
|
753
|
+
检查 Agent 是否存在
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
agent_id: Agent ID
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
是否存在
|
|
760
|
+
"""
|
|
761
|
+
params = [agent_id]
|
|
762
|
+
return self.contract_adapter.call("identity", "agentExists", params)
|
|
763
|
+
|
|
764
|
+
def get_agent_owner(self, agent_id: int) -> str:
|
|
765
|
+
"""
|
|
766
|
+
获取 Agent 的所有者地址
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
agent_id: Agent ID
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
所有者地址
|
|
773
|
+
"""
|
|
774
|
+
params = [agent_id]
|
|
775
|
+
return self.contract_adapter.call("identity", "ownerOf", params)
|
|
776
|
+
|
|
777
|
+
def total_agents(self) -> int:
|
|
778
|
+
"""
|
|
779
|
+
获取已注册的 Agent 总数
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Agent 总数
|
|
783
|
+
"""
|
|
784
|
+
return self.contract_adapter.call("identity", "totalAgents", [])
|
|
785
|
+
|
|
786
|
+
def get_agent_wallet(self, agent_id: int) -> str:
|
|
787
|
+
"""
|
|
788
|
+
获取 Agent 的钱包地址
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
agent_id: Agent ID
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
钱包地址(如果未设置返回零地址)
|
|
795
|
+
|
|
796
|
+
Example:
|
|
797
|
+
>>> wallet = sdk.get_agent_wallet(1)
|
|
798
|
+
>>> print(wallet) # "TWallet..."
|
|
799
|
+
"""
|
|
800
|
+
params = [agent_id]
|
|
801
|
+
return self.contract_adapter.call("identity", "getAgentWallet", params)
|
|
802
|
+
|
|
803
|
+
# ==================== Validation Registry 只读方法 ====================
|
|
804
|
+
|
|
805
|
+
def get_validation_status(self, request_hash: str) -> dict:
|
|
806
|
+
"""
|
|
807
|
+
获取验证状态 (Jan 2026 Update)
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
request_hash: 验证请求哈希(32 bytes)
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
验证结果字典,包含:
|
|
814
|
+
- validatorAddress: 验证者地址 (address(0) if no response yet)
|
|
815
|
+
- agentId: Agent ID (0 if no response yet)
|
|
816
|
+
- response: 验证评分 (0-100, or 0 if no response yet)
|
|
817
|
+
- tag: 标签 (string)
|
|
818
|
+
- lastUpdate: 最后更新时间戳 (0 if no response yet)
|
|
819
|
+
|
|
820
|
+
Note:
|
|
821
|
+
返回默认值表示请求待处理(无响应),不会抛出异常。
|
|
822
|
+
要区分不存在的请求和待处理的请求,请使用 request_exists()。
|
|
823
|
+
|
|
824
|
+
Example:
|
|
825
|
+
>>> result = sdk.get_validation_status("0x" + "aa" * 32)
|
|
826
|
+
>>> print(result["response"]) # 100
|
|
827
|
+
"""
|
|
828
|
+
params = [self._normalize_bytes32(request_hash)]
|
|
829
|
+
result = self.contract_adapter.call("validation", "getValidationStatus", params)
|
|
830
|
+
if isinstance(result, (list, tuple)) and len(result) >= 5:
|
|
831
|
+
return {
|
|
832
|
+
"validatorAddress": result[0],
|
|
833
|
+
"agentId": result[1],
|
|
834
|
+
"response": result[2],
|
|
835
|
+
"tag": result[3],
|
|
836
|
+
"lastUpdate": result[4],
|
|
837
|
+
}
|
|
838
|
+
return result
|
|
839
|
+
|
|
840
|
+
def get_validation(self, request_hash: str) -> dict:
|
|
841
|
+
"""
|
|
842
|
+
获取验证结果 (已弃用,请使用 get_validation_status)
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
request_hash: 验证请求哈希(32 bytes)
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
验证结果字典
|
|
849
|
+
"""
|
|
850
|
+
logger.warning("get_validation() is deprecated, use get_validation_status() instead")
|
|
851
|
+
return self.get_validation_status(request_hash)
|
|
852
|
+
|
|
853
|
+
def request_exists(self, request_hash: str) -> bool:
|
|
854
|
+
"""
|
|
855
|
+
检查验证请求是否存在 (Jan 2026 Update)
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
request_hash: 验证请求哈希(32 bytes)
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
是否存在
|
|
862
|
+
|
|
863
|
+
Example:
|
|
864
|
+
>>> exists = sdk.request_exists("0x" + "aa" * 32)
|
|
865
|
+
"""
|
|
866
|
+
params = [self._normalize_bytes32(request_hash)]
|
|
867
|
+
return self.contract_adapter.call("validation", "requestExists", params)
|
|
868
|
+
|
|
869
|
+
def get_validation_request(self, request_hash: str) -> dict:
|
|
870
|
+
"""
|
|
871
|
+
获取验证请求详情 (Jan 2026 Update)
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
request_hash: 验证请求哈希(32 bytes)
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
请求详情字典,包含:
|
|
878
|
+
- validatorAddress: 验证者地址
|
|
879
|
+
- agentId: Agent ID
|
|
880
|
+
- requestURI: 请求 URI
|
|
881
|
+
- timestamp: 请求时间戳
|
|
882
|
+
|
|
883
|
+
Example:
|
|
884
|
+
>>> request = sdk.get_validation_request("0x" + "aa" * 32)
|
|
885
|
+
>>> print(request["requestURI"])
|
|
886
|
+
"""
|
|
887
|
+
params = [self._normalize_bytes32(request_hash)]
|
|
888
|
+
result = self.contract_adapter.call("validation", "getRequest", params)
|
|
889
|
+
if isinstance(result, (list, tuple)) and len(result) >= 4:
|
|
890
|
+
return {
|
|
891
|
+
"validatorAddress": result[0],
|
|
892
|
+
"agentId": result[1],
|
|
893
|
+
"requestURI": result[2],
|
|
894
|
+
"timestamp": result[3],
|
|
895
|
+
}
|
|
896
|
+
return result
|
|
897
|
+
|
|
898
|
+
def get_validation_summary(
|
|
899
|
+
self,
|
|
900
|
+
agent_id: int,
|
|
901
|
+
validator_addresses: Optional[list[str]] = None,
|
|
902
|
+
tag: str = "",
|
|
903
|
+
) -> dict:
|
|
904
|
+
"""
|
|
905
|
+
获取 Agent 的验证汇总 (Jan 2026 Update)
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
agent_id: Agent ID
|
|
909
|
+
validator_addresses: 验证者地址列表(可选,用于过滤)
|
|
910
|
+
tag: 标签(可选)
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
汇总结果字典,包含:
|
|
914
|
+
- count: 验证数量
|
|
915
|
+
- averageResponse: 平均评分
|
|
916
|
+
|
|
917
|
+
Example:
|
|
918
|
+
>>> summary = sdk.get_validation_summary(1)
|
|
919
|
+
>>> print(f"Count: {summary['count']}, Avg: {summary['averageResponse']}")
|
|
920
|
+
"""
|
|
921
|
+
params = [agent_id, validator_addresses or [], tag]
|
|
922
|
+
result = self.contract_adapter.call("validation", "getSummary", params)
|
|
923
|
+
if isinstance(result, (list, tuple)) and len(result) >= 2:
|
|
924
|
+
return {
|
|
925
|
+
"count": result[0],
|
|
926
|
+
"averageResponse": result[1],
|
|
927
|
+
}
|
|
928
|
+
return result
|
|
929
|
+
|
|
930
|
+
def get_agent_validations(self, agent_id: int) -> list[str]:
|
|
931
|
+
"""
|
|
932
|
+
获取 Agent 的所有验证请求哈希 (Jan 2026 Update)
|
|
933
|
+
|
|
934
|
+
Args:
|
|
935
|
+
agent_id: Agent ID
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
请求哈希列表
|
|
939
|
+
"""
|
|
940
|
+
params = [agent_id]
|
|
941
|
+
return self.contract_adapter.call("validation", "getAgentValidations", params)
|
|
942
|
+
|
|
943
|
+
def get_validator_requests(self, validator_address: str) -> list[str]:
|
|
944
|
+
"""
|
|
945
|
+
获取验证者的所有验证请求哈希 (Jan 2026 Update)
|
|
946
|
+
|
|
947
|
+
Args:
|
|
948
|
+
validator_address: 验证者地址
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
请求哈希列表
|
|
952
|
+
"""
|
|
953
|
+
params = [validator_address]
|
|
954
|
+
return self.contract_adapter.call("validation", "getValidatorRequests", params)
|
|
955
|
+
|
|
956
|
+
# ==================== Reputation Registry 只读方法 ====================
|
|
957
|
+
|
|
958
|
+
def get_feedback_summary(
|
|
959
|
+
self,
|
|
960
|
+
agent_id: int,
|
|
961
|
+
client_addresses: Optional[list[str]] = None,
|
|
962
|
+
tag1: str = "",
|
|
963
|
+
tag2: str = "",
|
|
964
|
+
) -> dict:
|
|
965
|
+
"""
|
|
966
|
+
获取 Agent 的反馈汇总
|
|
967
|
+
|
|
968
|
+
Args:
|
|
969
|
+
agent_id: Agent ID
|
|
970
|
+
client_addresses: 客户端地址列表(可选,用于过滤)
|
|
971
|
+
tag1: 标签1(可选)
|
|
972
|
+
tag2: 标签2(可选)
|
|
973
|
+
|
|
974
|
+
Returns:
|
|
975
|
+
汇总结果字典,包含:
|
|
976
|
+
- count: 反馈数量
|
|
977
|
+
- averageScore: 平均评分
|
|
978
|
+
|
|
979
|
+
Example:
|
|
980
|
+
>>> summary = sdk.get_feedback_summary(1)
|
|
981
|
+
>>> print(f"Count: {summary['count']}, Avg: {summary['averageScore']}")
|
|
982
|
+
"""
|
|
983
|
+
params = [agent_id, client_addresses or [], tag1, tag2]
|
|
984
|
+
result = self.contract_adapter.call("reputation", "getSummary", params)
|
|
985
|
+
if isinstance(result, (list, tuple)) and len(result) >= 2:
|
|
986
|
+
return {
|
|
987
|
+
"count": result[0],
|
|
988
|
+
"averageScore": result[1],
|
|
989
|
+
}
|
|
990
|
+
return result
|
|
991
|
+
|
|
992
|
+
def read_feedback(
|
|
993
|
+
self,
|
|
994
|
+
agent_id: int,
|
|
995
|
+
client_address: str,
|
|
996
|
+
feedback_index: int,
|
|
997
|
+
) -> dict:
|
|
998
|
+
"""
|
|
999
|
+
读取单条反馈
|
|
1000
|
+
|
|
1001
|
+
Args:
|
|
1002
|
+
agent_id: Agent ID
|
|
1003
|
+
client_address: 客户端地址
|
|
1004
|
+
feedback_index: 反馈索引
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
反馈详情字典,包含:
|
|
1008
|
+
- score: 评分 (0-100)
|
|
1009
|
+
- tag1: 标签1
|
|
1010
|
+
- tag2: 标签2
|
|
1011
|
+
- isRevoked: 是否已撤销
|
|
1012
|
+
|
|
1013
|
+
Example:
|
|
1014
|
+
>>> feedback = sdk.read_feedback(1, "TClient...", 0)
|
|
1015
|
+
>>> print(f"Score: {feedback['score']}")
|
|
1016
|
+
"""
|
|
1017
|
+
params = [agent_id, client_address, feedback_index]
|
|
1018
|
+
result = self.contract_adapter.call("reputation", "readFeedback", params)
|
|
1019
|
+
if isinstance(result, (list, tuple)) and len(result) >= 4:
|
|
1020
|
+
return {
|
|
1021
|
+
"score": result[0],
|
|
1022
|
+
"tag1": result[1],
|
|
1023
|
+
"tag2": result[2],
|
|
1024
|
+
"isRevoked": result[3],
|
|
1025
|
+
}
|
|
1026
|
+
return result
|
|
1027
|
+
|
|
1028
|
+
def get_feedback_clients(self, agent_id: int) -> list[str]:
|
|
1029
|
+
"""
|
|
1030
|
+
获取给 Agent 提交过反馈的所有客户端地址
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
agent_id: Agent ID
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
客户端地址列表
|
|
1037
|
+
"""
|
|
1038
|
+
params = [agent_id]
|
|
1039
|
+
return self.contract_adapter.call("reputation", "getClients", params)
|
|
1040
|
+
|
|
1041
|
+
def get_last_feedback_index(self, agent_id: int, client_address: str) -> int:
|
|
1042
|
+
"""
|
|
1043
|
+
获取客户端对 Agent 的最后一条反馈索引
|
|
1044
|
+
|
|
1045
|
+
Args:
|
|
1046
|
+
agent_id: Agent ID
|
|
1047
|
+
client_address: 客户端地址
|
|
1048
|
+
|
|
1049
|
+
Returns:
|
|
1050
|
+
最后一条反馈的索引
|
|
1051
|
+
"""
|
|
1052
|
+
params = [agent_id, client_address]
|
|
1053
|
+
return self.contract_adapter.call("reputation", "getLastIndex", params)
|
|
1054
|
+
|
|
1055
|
+
# ==================== Reputation Registry 写入方法 ====================
|
|
1056
|
+
|
|
1057
|
+
def revoke_feedback(
|
|
1058
|
+
self,
|
|
1059
|
+
agent_id: int,
|
|
1060
|
+
feedback_index: int,
|
|
1061
|
+
signer: Optional[Signer] = None,
|
|
1062
|
+
) -> str:
|
|
1063
|
+
"""
|
|
1064
|
+
撤销反馈
|
|
1065
|
+
|
|
1066
|
+
只有原始提交者可以撤销自己的反馈。
|
|
1067
|
+
|
|
1068
|
+
Args:
|
|
1069
|
+
agent_id: Agent ID
|
|
1070
|
+
feedback_index: 反馈索引
|
|
1071
|
+
signer: 自定义签名器(可选)
|
|
1072
|
+
|
|
1073
|
+
Returns:
|
|
1074
|
+
交易 ID
|
|
1075
|
+
|
|
1076
|
+
Example:
|
|
1077
|
+
>>> tx_id = sdk.revoke_feedback(agent_id=1, feedback_index=0)
|
|
1078
|
+
"""
|
|
1079
|
+
signer = signer or self.signer
|
|
1080
|
+
if signer is None:
|
|
1081
|
+
raise SignerNotAvailableError()
|
|
1082
|
+
|
|
1083
|
+
params = [agent_id, feedback_index]
|
|
1084
|
+
logger.debug("revoke_feedback: agent_id=%d, index=%d", agent_id, feedback_index)
|
|
1085
|
+
return self.contract_adapter.send("reputation", "revokeFeedback", params, signer)
|
|
1086
|
+
|
|
1087
|
+
def append_feedback_response(
|
|
1088
|
+
self,
|
|
1089
|
+
agent_id: int,
|
|
1090
|
+
client_address: str,
|
|
1091
|
+
feedback_index: int,
|
|
1092
|
+
response_uri: str,
|
|
1093
|
+
response_hash: Optional[str] = None,
|
|
1094
|
+
signer: Optional[Signer] = None,
|
|
1095
|
+
) -> str:
|
|
1096
|
+
"""
|
|
1097
|
+
追加反馈响应
|
|
1098
|
+
|
|
1099
|
+
任何人都可以追加响应(如 Agent 展示退款证明,或数据分析服务标记垃圾反馈)。
|
|
1100
|
+
|
|
1101
|
+
Args:
|
|
1102
|
+
agent_id: Agent ID
|
|
1103
|
+
client_address: 原始反馈的客户端地址
|
|
1104
|
+
feedback_index: 反馈索引
|
|
1105
|
+
response_uri: 响应文件 URI
|
|
1106
|
+
response_hash: 响应文件哈希(可选,IPFS URI 不需要)
|
|
1107
|
+
signer: 自定义签名器(可选)
|
|
1108
|
+
|
|
1109
|
+
Returns:
|
|
1110
|
+
交易 ID
|
|
1111
|
+
|
|
1112
|
+
Example:
|
|
1113
|
+
>>> tx_id = sdk.append_feedback_response(
|
|
1114
|
+
... agent_id=1,
|
|
1115
|
+
... client_address="TClient...",
|
|
1116
|
+
... feedback_index=0,
|
|
1117
|
+
... response_uri="ipfs://Qm...",
|
|
1118
|
+
... )
|
|
1119
|
+
"""
|
|
1120
|
+
signer = signer or self.signer
|
|
1121
|
+
if signer is None:
|
|
1122
|
+
raise SignerNotAvailableError()
|
|
1123
|
+
|
|
1124
|
+
params = [
|
|
1125
|
+
agent_id,
|
|
1126
|
+
client_address,
|
|
1127
|
+
feedback_index,
|
|
1128
|
+
response_uri,
|
|
1129
|
+
self._normalize_bytes32(response_hash),
|
|
1130
|
+
]
|
|
1131
|
+
logger.debug("append_feedback_response: agent_id=%d, index=%d", agent_id, feedback_index)
|
|
1132
|
+
return self.contract_adapter.send("reputation", "appendResponse", params, signer)
|
|
1133
|
+
|
|
1134
|
+
def build_feedback_auth(
|
|
1135
|
+
self,
|
|
1136
|
+
agent_id: int,
|
|
1137
|
+
client_addr: str,
|
|
1138
|
+
index_limit: int,
|
|
1139
|
+
expiry: int,
|
|
1140
|
+
chain_id: Optional[int],
|
|
1141
|
+
identity_registry: str,
|
|
1142
|
+
signer: Optional[Signer] = None,
|
|
1143
|
+
) -> str:
|
|
1144
|
+
"""
|
|
1145
|
+
构建反馈授权签名 (已弃用 - Jan 2026 Update)
|
|
1146
|
+
|
|
1147
|
+
警告:Jan 2026 更新移除了 feedbackAuth 预授权机制。
|
|
1148
|
+
现在任何人都可以直接调用 giveFeedback() 提交反馈,无需预授权。
|
|
1149
|
+
此方法保留仅为向后兼容,将在未来版本中移除。
|
|
1150
|
+
|
|
1151
|
+
Args:
|
|
1152
|
+
agent_id: Agent ID
|
|
1153
|
+
client_addr: 被授权的客户端地址
|
|
1154
|
+
index_limit: 反馈索引上限
|
|
1155
|
+
expiry: 授权过期时间(Unix 时间戳)
|
|
1156
|
+
chain_id: 链 ID(可选,会自动解析)
|
|
1157
|
+
identity_registry: IdentityRegistry 合约地址
|
|
1158
|
+
signer: 自定义签名器(可选)
|
|
1159
|
+
|
|
1160
|
+
Returns:
|
|
1161
|
+
反馈授权签名(0x 前缀的十六进制字符串)
|
|
1162
|
+
|
|
1163
|
+
Raises:
|
|
1164
|
+
DeprecationWarning: 此方法已弃用
|
|
1165
|
+
"""
|
|
1166
|
+
import warnings
|
|
1167
|
+
warnings.warn(
|
|
1168
|
+
"build_feedback_auth() is deprecated since Jan 2026 Update. "
|
|
1169
|
+
"feedbackAuth pre-authorization has been removed from the contract. "
|
|
1170
|
+
"Use submit_reputation() directly without feedbackAuth.",
|
|
1171
|
+
DeprecationWarning,
|
|
1172
|
+
stacklevel=2,
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
signer = signer or self.signer
|
|
1176
|
+
if signer is None:
|
|
1177
|
+
raise SignerNotAvailableError()
|
|
1178
|
+
|
|
1179
|
+
if chain_id is None:
|
|
1180
|
+
chain_id = self.resolve_chain_id()
|
|
1181
|
+
if chain_id is None:
|
|
1182
|
+
raise ChainIdResolutionError(self.config.rpc_url)
|
|
1183
|
+
|
|
1184
|
+
signer_addr = signer.get_address()
|
|
1185
|
+
|
|
1186
|
+
# 构建 feedbackAuth 结构体 (legacy format)
|
|
1187
|
+
struct_bytes = b"".join(
|
|
1188
|
+
[
|
|
1189
|
+
self._abi_encode_uint(agent_id),
|
|
1190
|
+
self._abi_encode_address(client_addr),
|
|
1191
|
+
self._abi_encode_uint(index_limit),
|
|
1192
|
+
self._abi_encode_uint(expiry),
|
|
1193
|
+
self._abi_encode_uint(chain_id),
|
|
1194
|
+
self._abi_encode_address(identity_registry),
|
|
1195
|
+
self._abi_encode_address(signer_addr),
|
|
1196
|
+
]
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
# EIP-191 签名
|
|
1200
|
+
struct_hash = keccak256_bytes(struct_bytes)
|
|
1201
|
+
message = keccak256_bytes(b"\x19Ethereum Signed Message:\n32" + struct_hash)
|
|
1202
|
+
signature = self._normalize_bytes(signer.sign_message(message))
|
|
1203
|
+
|
|
1204
|
+
# 规范化签名(处理 v 值和 s 值)
|
|
1205
|
+
if len(signature) == 65:
|
|
1206
|
+
v = signature[-1]
|
|
1207
|
+
if v in (0, 1):
|
|
1208
|
+
v += 27
|
|
1209
|
+
r = int.from_bytes(signature[:32], byteorder="big")
|
|
1210
|
+
s = int.from_bytes(signature[32:64], byteorder="big")
|
|
1211
|
+
secp256k1_n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
1212
|
+
if s > secp256k1_n // 2:
|
|
1213
|
+
s = secp256k1_n - s
|
|
1214
|
+
v = 27 if v == 28 else 28
|
|
1215
|
+
signature = (
|
|
1216
|
+
r.to_bytes(32, byteorder="big")
|
|
1217
|
+
+ s.to_bytes(32, byteorder="big")
|
|
1218
|
+
+ bytes([v])
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
logger.debug("build_feedback_auth (DEPRECATED): agent_id=%d, client=%s", agent_id, client_addr[:12])
|
|
1222
|
+
return "0x" + (struct_bytes + signature).hex()
|
|
1223
|
+
|
|
1224
|
+
@staticmethod
|
|
1225
|
+
def _normalize_metadata_entries(entries: list[dict]) -> list[tuple]:
|
|
1226
|
+
"""
|
|
1227
|
+
规范化元数据条目为 tuple 格式 (Jan 2026 Update)
|
|
1228
|
+
|
|
1229
|
+
合约期望的格式是 (string metadataKey, bytes metadataValue) 的 tuple 数组
|
|
1230
|
+
|
|
1231
|
+
注意:Jan 2026 更新将 struct 字段名从 (key, value) 改为 (metadataKey, metadataValue)
|
|
1232
|
+
"""
|
|
1233
|
+
if not isinstance(entries, list):
|
|
1234
|
+
raise TypeError("metadata must be a list of {key,value} objects")
|
|
1235
|
+
normalized = []
|
|
1236
|
+
for entry in entries:
|
|
1237
|
+
if not isinstance(entry, dict):
|
|
1238
|
+
raise TypeError("metadata entry must be an object")
|
|
1239
|
+
# 支持新旧两种字段名
|
|
1240
|
+
key = entry.get("metadataKey") or entry.get("key")
|
|
1241
|
+
value = entry.get("metadataValue") or entry.get("value")
|
|
1242
|
+
if not key:
|
|
1243
|
+
raise ValueError("metadata entry missing key (metadataKey or key)")
|
|
1244
|
+
if isinstance(value, bytes):
|
|
1245
|
+
value_bytes = value
|
|
1246
|
+
elif isinstance(value, str):
|
|
1247
|
+
if value.startswith("0x") and _is_hex_string(value[2:]):
|
|
1248
|
+
value_bytes = bytes.fromhex(value[2:])
|
|
1249
|
+
else:
|
|
1250
|
+
value_bytes = value.encode("utf-8")
|
|
1251
|
+
elif value is None:
|
|
1252
|
+
value_bytes = b""
|
|
1253
|
+
else:
|
|
1254
|
+
raise TypeError("metadata value must be bytes or string")
|
|
1255
|
+
# 返回 tuple 格式,符合 Solidity struct 编码要求
|
|
1256
|
+
# 字段名为 (metadataKey, metadataValue) 但 tuple 编码只需要值
|
|
1257
|
+
normalized.append((key, value_bytes))
|
|
1258
|
+
return normalized
|
|
1259
|
+
|
|
1260
|
+
def resolve_chain_id(self) -> Optional[int]:
|
|
1261
|
+
"""
|
|
1262
|
+
从 RPC 节点解析 Chain ID
|
|
1263
|
+
|
|
1264
|
+
Returns:
|
|
1265
|
+
Chain ID,解析失败返回 None
|
|
1266
|
+
"""
|
|
1267
|
+
rpc_url = self.config.rpc_url
|
|
1268
|
+
if not rpc_url:
|
|
1269
|
+
return None
|
|
1270
|
+
url = rpc_url.rstrip("/") + "/jsonrpc"
|
|
1271
|
+
try:
|
|
1272
|
+
response = httpx.post(
|
|
1273
|
+
url,
|
|
1274
|
+
json={"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1},
|
|
1275
|
+
timeout=self.config.timeout,
|
|
1276
|
+
)
|
|
1277
|
+
response.raise_for_status()
|
|
1278
|
+
result = response.json().get("result")
|
|
1279
|
+
if isinstance(result, str) and result.startswith("0x"):
|
|
1280
|
+
return int(result, 16)
|
|
1281
|
+
except Exception as e:
|
|
1282
|
+
logger.warning("Failed to resolve chain ID: %s", e)
|
|
1283
|
+
return None
|
|
1284
|
+
return None
|
|
1285
|
+
|
|
1286
|
+
def build_commitment(self, order_params: dict) -> str:
|
|
1287
|
+
"""
|
|
1288
|
+
构建订单承诺哈希
|
|
1289
|
+
|
|
1290
|
+
对订单参数进行规范化 JSON 序列化后计算 keccak256 哈希。
|
|
1291
|
+
|
|
1292
|
+
Args:
|
|
1293
|
+
order_params: 订单参数字典
|
|
1294
|
+
|
|
1295
|
+
Returns:
|
|
1296
|
+
承诺哈希(0x 前缀)
|
|
1297
|
+
|
|
1298
|
+
Example:
|
|
1299
|
+
>>> commitment = sdk.build_commitment({
|
|
1300
|
+
... "asset": "TRX/USDT",
|
|
1301
|
+
... "amount": 100.0,
|
|
1302
|
+
... "slippage": 0.01,
|
|
1303
|
+
... })
|
|
1304
|
+
"""
|
|
1305
|
+
payload = canonical_json(order_params)
|
|
1306
|
+
return keccak256_hex(payload)
|
|
1307
|
+
|
|
1308
|
+
def compute_request_hash(self, request_payload: str | dict) -> str:
|
|
1309
|
+
"""
|
|
1310
|
+
计算请求数据哈希
|
|
1311
|
+
|
|
1312
|
+
Args:
|
|
1313
|
+
request_payload: 请求数据(字典或 JSON 字符串)
|
|
1314
|
+
|
|
1315
|
+
Returns:
|
|
1316
|
+
请求哈希(0x 前缀)
|
|
1317
|
+
"""
|
|
1318
|
+
if isinstance(request_payload, dict):
|
|
1319
|
+
payload_bytes = canonical_json(request_payload)
|
|
1320
|
+
else:
|
|
1321
|
+
payload_bytes = str(request_payload).encode("utf-8")
|
|
1322
|
+
return keccak256_hex(payload_bytes)
|
|
1323
|
+
|
|
1324
|
+
def dump_canonical(self, payload: dict) -> str:
|
|
1325
|
+
"""
|
|
1326
|
+
规范化 JSON 序列化
|
|
1327
|
+
|
|
1328
|
+
Args:
|
|
1329
|
+
payload: 待序列化的字典
|
|
1330
|
+
|
|
1331
|
+
Returns:
|
|
1332
|
+
规范化的 JSON 字符串(键排序,无空格)
|
|
1333
|
+
"""
|
|
1334
|
+
return canonical_json_str(payload)
|
|
1335
|
+
|
|
1336
|
+
def build_a2a_signature(
|
|
1337
|
+
self,
|
|
1338
|
+
action_commitment: str,
|
|
1339
|
+
timestamp: int,
|
|
1340
|
+
caller_address: str,
|
|
1341
|
+
signer: Optional[Signer] = None,
|
|
1342
|
+
) -> str:
|
|
1343
|
+
"""
|
|
1344
|
+
构建 A2A 请求签名
|
|
1345
|
+
|
|
1346
|
+
Args:
|
|
1347
|
+
action_commitment: 操作承诺哈希
|
|
1348
|
+
timestamp: 时间戳
|
|
1349
|
+
caller_address: 调用方地址
|
|
1350
|
+
signer: 自定义签名器(可选)
|
|
1351
|
+
|
|
1352
|
+
Returns:
|
|
1353
|
+
签名(0x 前缀)
|
|
1354
|
+
"""
|
|
1355
|
+
signer = signer or self.signer
|
|
1356
|
+
if signer is None:
|
|
1357
|
+
raise SignerNotAvailableError()
|
|
1358
|
+
|
|
1359
|
+
payload = {
|
|
1360
|
+
"actionCommitment": action_commitment,
|
|
1361
|
+
"timestamp": timestamp,
|
|
1362
|
+
"callerAddress": caller_address,
|
|
1363
|
+
}
|
|
1364
|
+
message = keccak256_bytes(canonical_json(payload))
|
|
1365
|
+
return signer.sign_message(message)
|
|
1366
|
+
|
|
1367
|
+
def build_market_order_quote_request(self, asset: str, amount: float, slippage: float = 0.01) -> dict:
|
|
1368
|
+
"""
|
|
1369
|
+
构建市价单报价请求
|
|
1370
|
+
|
|
1371
|
+
Args:
|
|
1372
|
+
asset: 交易对(如 "TRX/USDT")
|
|
1373
|
+
amount: 交易数量
|
|
1374
|
+
slippage: 滑点容忍度(默认 1%)
|
|
1375
|
+
|
|
1376
|
+
Returns:
|
|
1377
|
+
报价请求字典
|
|
1378
|
+
"""
|
|
1379
|
+
return {
|
|
1380
|
+
"asset": asset,
|
|
1381
|
+
"amount": amount,
|
|
1382
|
+
"slippage": slippage,
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
def build_market_order_new_request(
|
|
1386
|
+
self,
|
|
1387
|
+
asset: str,
|
|
1388
|
+
amount: float,
|
|
1389
|
+
payment_tx_hash: str,
|
|
1390
|
+
slippage: float = 0.01,
|
|
1391
|
+
) -> dict:
|
|
1392
|
+
"""
|
|
1393
|
+
构建新建市价单请求
|
|
1394
|
+
|
|
1395
|
+
Args:
|
|
1396
|
+
asset: 交易对
|
|
1397
|
+
amount: 交易数量
|
|
1398
|
+
payment_tx_hash: 支付交易哈希
|
|
1399
|
+
slippage: 滑点容忍度
|
|
1400
|
+
|
|
1401
|
+
Returns:
|
|
1402
|
+
新建订单请求字典
|
|
1403
|
+
"""
|
|
1404
|
+
return {
|
|
1405
|
+
"asset": asset,
|
|
1406
|
+
"amount": amount,
|
|
1407
|
+
"slippage": slippage,
|
|
1408
|
+
"paymentTxHash": payment_tx_hash,
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
def build_x402_quote_request(self, order_params: dict) -> dict:
|
|
1412
|
+
"""
|
|
1413
|
+
构建 X402 报价请求
|
|
1414
|
+
|
|
1415
|
+
Args:
|
|
1416
|
+
order_params: 订单参数
|
|
1417
|
+
|
|
1418
|
+
Returns:
|
|
1419
|
+
X402 报价请求字典
|
|
1420
|
+
"""
|
|
1421
|
+
return {"orderParams": order_params}
|
|
1422
|
+
|
|
1423
|
+
def build_x402_execute_request(
|
|
1424
|
+
self,
|
|
1425
|
+
action_commitment: str,
|
|
1426
|
+
order_params: dict,
|
|
1427
|
+
payment_tx_hash: str,
|
|
1428
|
+
timestamp: int,
|
|
1429
|
+
caller_address: str,
|
|
1430
|
+
include_signature: bool = True,
|
|
1431
|
+
) -> dict:
|
|
1432
|
+
"""
|
|
1433
|
+
构建 X402 执行请求
|
|
1434
|
+
|
|
1435
|
+
Args:
|
|
1436
|
+
action_commitment: 操作承诺哈希
|
|
1437
|
+
order_params: 订单参数
|
|
1438
|
+
payment_tx_hash: 支付交易哈希
|
|
1439
|
+
timestamp: 时间戳
|
|
1440
|
+
caller_address: 调用方地址
|
|
1441
|
+
include_signature: 是否包含签名
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
X402 执行请求字典
|
|
1445
|
+
"""
|
|
1446
|
+
payload = {
|
|
1447
|
+
"actionCommitment": action_commitment,
|
|
1448
|
+
"orderParams": order_params,
|
|
1449
|
+
"paymentTxHash": payment_tx_hash,
|
|
1450
|
+
"timestamp": timestamp,
|
|
1451
|
+
}
|
|
1452
|
+
if include_signature:
|
|
1453
|
+
payload["signature"] = self.build_a2a_signature(
|
|
1454
|
+
action_commitment, timestamp, caller_address
|
|
1455
|
+
)
|
|
1456
|
+
return payload
|
|
1457
|
+
|
|
1458
|
+
def build_payment_signature(
|
|
1459
|
+
self,
|
|
1460
|
+
action_commitment: str,
|
|
1461
|
+
payment_address: str,
|
|
1462
|
+
amount: str,
|
|
1463
|
+
timestamp: int,
|
|
1464
|
+
signer: Optional[Signer] = None,
|
|
1465
|
+
) -> str:
|
|
1466
|
+
"""
|
|
1467
|
+
构建支付签名
|
|
1468
|
+
|
|
1469
|
+
Args:
|
|
1470
|
+
action_commitment: 操作承诺哈希
|
|
1471
|
+
payment_address: 收款地址
|
|
1472
|
+
amount: 支付金额
|
|
1473
|
+
timestamp: 时间戳
|
|
1474
|
+
signer: 自定义签名器(可选)
|
|
1475
|
+
|
|
1476
|
+
Returns:
|
|
1477
|
+
支付签名(0x 前缀)
|
|
1478
|
+
"""
|
|
1479
|
+
signer = signer or self.signer
|
|
1480
|
+
if signer is None:
|
|
1481
|
+
raise SignerNotAvailableError()
|
|
1482
|
+
|
|
1483
|
+
payload = {
|
|
1484
|
+
"actionCommitment": action_commitment,
|
|
1485
|
+
"paymentAddress": payment_address,
|
|
1486
|
+
"amount": amount,
|
|
1487
|
+
"timestamp": timestamp,
|
|
1488
|
+
}
|
|
1489
|
+
message = keccak256_bytes(canonical_json(payload))
|
|
1490
|
+
return signer.sign_message(message)
|
|
1491
|
+
|
|
1492
|
+
@staticmethod
|
|
1493
|
+
def _normalize_bytes32(value: Optional[str | bytes]) -> bytes:
|
|
1494
|
+
"""规范化为 32 字节"""
|
|
1495
|
+
if value is None:
|
|
1496
|
+
return b"\x00" * 32
|
|
1497
|
+
if isinstance(value, bytes):
|
|
1498
|
+
if len(value) < 32:
|
|
1499
|
+
return value.ljust(32, b"\x00")
|
|
1500
|
+
return value[:32]
|
|
1501
|
+
cleaned = value[2:] if value.startswith("0x") else value
|
|
1502
|
+
if not cleaned:
|
|
1503
|
+
return b"\x00" * 32
|
|
1504
|
+
raw = bytes.fromhex(cleaned)
|
|
1505
|
+
if len(raw) < 32:
|
|
1506
|
+
return raw.ljust(32, b"\x00")
|
|
1507
|
+
return raw[:32]
|
|
1508
|
+
|
|
1509
|
+
@staticmethod
|
|
1510
|
+
def _normalize_bytes(value: Optional[str | bytes]) -> bytes:
|
|
1511
|
+
"""规范化为字节"""
|
|
1512
|
+
if value is None:
|
|
1513
|
+
return b""
|
|
1514
|
+
if isinstance(value, bytes):
|
|
1515
|
+
return value
|
|
1516
|
+
cleaned = value[2:] if value.startswith("0x") else value
|
|
1517
|
+
if not cleaned:
|
|
1518
|
+
return b""
|
|
1519
|
+
return bytes.fromhex(cleaned)
|
|
1520
|
+
|
|
1521
|
+
@staticmethod
|
|
1522
|
+
def _abi_encode_uint(value: int) -> bytes:
|
|
1523
|
+
"""ABI 编码无符号整数(32 字节)"""
|
|
1524
|
+
return int(value).to_bytes(32, byteorder="big")
|
|
1525
|
+
|
|
1526
|
+
@staticmethod
|
|
1527
|
+
def _abi_encode_address(address: str) -> bytes:
|
|
1528
|
+
"""
|
|
1529
|
+
ABI 编码地址(32 字节,左填充零)
|
|
1530
|
+
|
|
1531
|
+
支持 TRON base58 地址和 EVM hex 地址。
|
|
1532
|
+
|
|
1533
|
+
Raises:
|
|
1534
|
+
InvalidAddressError: 地址格式无效
|
|
1535
|
+
"""
|
|
1536
|
+
addr = address
|
|
1537
|
+
if addr.startswith("T"):
|
|
1538
|
+
try:
|
|
1539
|
+
from tronpy.keys import to_hex_address
|
|
1540
|
+
except Exception as exc:
|
|
1541
|
+
raise InvalidAddressError(address, "tronpy required for base58") from exc
|
|
1542
|
+
addr = to_hex_address(addr)
|
|
1543
|
+
if addr.startswith("0x"):
|
|
1544
|
+
addr = addr[2:]
|
|
1545
|
+
if len(addr) == 42 and addr.startswith("41"):
|
|
1546
|
+
addr = addr[2:]
|
|
1547
|
+
if len(addr) != 40:
|
|
1548
|
+
raise InvalidAddressError(address, "expected 20 bytes hex")
|
|
1549
|
+
return bytes.fromhex(addr).rjust(32, b"\x00")
|