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/client.py ADDED
@@ -0,0 +1,202 @@
1
+ """
2
+ TRC-8004 SDK Agent 客户端模块
3
+
4
+ 提供智能 HTTP 客户端,自动解析 Agent 元数据中的端点。
5
+
6
+ Classes:
7
+ AgentClient: 智能 Agent HTTP 客户端
8
+
9
+ Example:
10
+ >>> from sdk.client import AgentClient
11
+ >>> client = AgentClient(
12
+ ... metadata=agent_metadata,
13
+ ... base_url="https://agent.example.com",
14
+ ... )
15
+ >>> response = client.post("quote", {"asset": "TRX/USDT", "amount": 100})
16
+
17
+ Note:
18
+ - 支持从 Agent 元数据自动解析端点
19
+ - 遵循 A2A 协议的 URL 约定
20
+ - 支持 mock 模式用于测试
21
+ """
22
+
23
+ from typing import Dict, Any, Optional
24
+
25
+ import httpx
26
+
27
+
28
+ class AgentClient:
29
+ """
30
+ 智能 Agent HTTP 客户端。
31
+
32
+ 根据 Agent 元数据自动解析端点 URL,支持:
33
+ - 从 metadata.url 获取基础 URL
34
+ - 从 metadata.endpoints 获取 A2A 端点
35
+ - 从 metadata.skills 获取特定能力的端点
36
+ - 使用 A2A 协议约定构造 URL
37
+
38
+ Attributes:
39
+ metadata: Agent 元数据字典
40
+ base_url: 基础 URL(优先级低于 metadata)
41
+
42
+ Args:
43
+ metadata: Agent 元数据,通常从 Central Service 获取
44
+ base_url: 基础 URL,作为 metadata 的回退
45
+
46
+ Example:
47
+ >>> # 使用元数据
48
+ >>> client = AgentClient(metadata={
49
+ ... "url": "https://agent.example.com",
50
+ ... "skills": [{"id": "quote", "endpoint": "/custom/quote"}],
51
+ ... })
52
+ >>> client.resolve_url("quote")
53
+ 'https://agent.example.com/custom/quote'
54
+ >>>
55
+ >>> # 使用基础 URL
56
+ >>> client = AgentClient(base_url="https://agent.example.com")
57
+ >>> client.resolve_url("execute")
58
+ 'https://agent.example.com/a2a/execute'
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ metadata: Optional[Dict[str, Any]] = None,
64
+ base_url: Optional[str] = None,
65
+ ) -> None:
66
+ """
67
+ 初始化 Agent 客户端。
68
+
69
+ Args:
70
+ metadata: Agent 元数据字典,包含 url、endpoints、skills 等
71
+ base_url: 基础 URL,当 metadata 中没有 URL 时使用
72
+ """
73
+ self.metadata = metadata or {}
74
+ self.base_url = (base_url or "").rstrip("/")
75
+
76
+ def resolve_url(self, capability: str) -> str:
77
+ """
78
+ 解析能力/技能对应的完整 URL。
79
+
80
+ 解析优先级:
81
+ 1. 检查 mock 模式
82
+ 2. 从 metadata.url 或 base_url 获取基础 URL
83
+ 3. 从 metadata.endpoints 查找 A2A 端点
84
+ 4. 从 metadata.skills 查找特定能力的端点
85
+ 5. 使用 A2A 协议约定:{base_url}/a2a/{capability}
86
+
87
+ Args:
88
+ capability: 能力/技能名称(如 'quote', 'execute')
89
+
90
+ Returns:
91
+ 完整的端点 URL
92
+
93
+ Raises:
94
+ ValueError: 无法找到基础 URL
95
+
96
+ Example:
97
+ >>> client = AgentClient(base_url="https://agent.example.com")
98
+ >>> client.resolve_url("quote")
99
+ 'https://agent.example.com/a2a/quote'
100
+ """
101
+ # 0. 检查 Mock 模式
102
+ if self.base_url == "mock":
103
+ return "mock"
104
+
105
+ # 1. 获取基础 URL
106
+ base_url = self.metadata.get("url") or self.base_url
107
+ if not base_url:
108
+ # 尝试从 endpoints 获取 A2A 端点
109
+ for endpoint in self.metadata.get("endpoints", []):
110
+ if endpoint.get("name", "").lower() == "a2a":
111
+ base_url = endpoint.get("endpoint", "")
112
+ break
113
+
114
+ if not base_url:
115
+ raise ValueError(f"No Base URL found for agent to capability '{capability}'")
116
+
117
+ # 2. 检查 skills 中是否有特定端点
118
+ skills = self.metadata.get("skills", [])
119
+ if skills:
120
+ skill = next((s for s in skills if s.get("id") == capability), None)
121
+ if skill:
122
+ endpoint = skill.get("endpoint") or skill.get("path")
123
+ if endpoint:
124
+ # 绝对 URL
125
+ if endpoint.startswith("http://") or endpoint.startswith("https://"):
126
+ return endpoint
127
+ # 相对路径
128
+ return f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
129
+
130
+ # 3. 使用 A2A 协议约定
131
+ return f"{base_url.rstrip('/')}/a2a/{capability}"
132
+
133
+ def post(
134
+ self,
135
+ capability: str,
136
+ json_data: Dict[str, Any],
137
+ timeout: float = 10.0,
138
+ ) -> Dict[str, Any]:
139
+ """
140
+ 向指定能力端点发送 POST 请求。
141
+
142
+ Args:
143
+ capability: 能力/技能名称
144
+ json_data: 请求体 JSON 数据
145
+ timeout: 请求超时时间(秒)
146
+
147
+ Returns:
148
+ 响应 JSON 数据
149
+
150
+ Raises:
151
+ ValueError: 无法解析 URL
152
+ httpx.HTTPStatusError: HTTP 请求失败
153
+ httpx.TimeoutException: 请求超时
154
+
155
+ Example:
156
+ >>> response = client.post("quote", {
157
+ ... "asset": "TRX/USDT",
158
+ ... "amount": 100,
159
+ ... })
160
+ """
161
+ url = self.resolve_url(capability)
162
+ if url == "mock":
163
+ return {"mock": True}
164
+
165
+ with httpx.Client(timeout=timeout) as client:
166
+ response = client.post(url, json=json_data)
167
+ response.raise_for_status()
168
+ return response.json()
169
+
170
+ def get(
171
+ self,
172
+ capability: str,
173
+ params: Optional[Dict[str, Any]] = None,
174
+ timeout: float = 10.0,
175
+ ) -> Dict[str, Any]:
176
+ """
177
+ 向指定能力端点发送 GET 请求。
178
+
179
+ Args:
180
+ capability: 能力/技能名称
181
+ params: URL 查询参数
182
+ timeout: 请求超时时间(秒)
183
+
184
+ Returns:
185
+ 响应 JSON 数据
186
+
187
+ Raises:
188
+ ValueError: 无法解析 URL
189
+ httpx.HTTPStatusError: HTTP 请求失败
190
+ httpx.TimeoutException: 请求超时
191
+
192
+ Example:
193
+ >>> response = client.get("status", params={"order_id": "123"})
194
+ """
195
+ url = self.resolve_url(capability)
196
+ if url == "mock":
197
+ return {"mock": True}
198
+
199
+ with httpx.Client(timeout=timeout) as client:
200
+ response = client.get(url, params=params)
201
+ response.raise_for_status()
202
+ return response.json()
@@ -0,0 +1,489 @@
1
+ """
2
+ TRC-8004 合约适配器
3
+
4
+ 提供与不同区块链交互的抽象层,支持:
5
+ - DummyContractAdapter: 本地开发/测试
6
+ - TronContractAdapter: TRON 区块链
7
+ - (未来) EVMContractAdapter: EVM 兼容链
8
+ """
9
+
10
+ import logging
11
+ import time
12
+ from typing import Any, List, Optional
13
+
14
+ from .exceptions import (
15
+ ContractCallError,
16
+ ContractFunctionNotFoundError,
17
+ InsufficientEnergyError,
18
+ MissingContractAddressError,
19
+ NetworkError,
20
+ TransactionFailedError,
21
+ )
22
+ from .retry import RetryConfig, DEFAULT_RETRY_CONFIG, retry
23
+ from .signer import Signer
24
+
25
+ logger = logging.getLogger("trc8004.adapter")
26
+
27
+
28
+ class ContractAdapter:
29
+ """
30
+ 合约适配器抽象基类
31
+
32
+ 定义与区块链合约交互的标准接口。
33
+ """
34
+
35
+ def call(self, contract: str, method: str, params: List[Any]) -> Any:
36
+ """
37
+ 调用合约只读方法
38
+
39
+ Args:
40
+ contract: 合约名称 ("identity", "validation", "reputation")
41
+ method: 方法名
42
+ params: 参数列表
43
+
44
+ Returns:
45
+ 调用结果
46
+ """
47
+ raise NotImplementedError
48
+
49
+ def send(self, contract: str, method: str, params: List[Any], signer: Signer) -> str:
50
+ """
51
+ 发送合约交易
52
+
53
+ Args:
54
+ contract: 合约名称
55
+ method: 方法名
56
+ params: 参数列表
57
+ signer: 签名器
58
+
59
+ Returns:
60
+ 交易 ID
61
+ """
62
+ raise NotImplementedError
63
+
64
+
65
+ class DummyContractAdapter(ContractAdapter):
66
+ """
67
+ 本地测试用适配器
68
+
69
+ 返回确定性的交易 ID,不进行实际的区块链交互。
70
+ 适用于单元测试和本地开发。
71
+ """
72
+
73
+ def call(self, contract: str, method: str, params: List[Any]) -> Any:
74
+ return {"contract": contract, "method": method, "params": params}
75
+
76
+ def send(self, contract: str, method: str, params: List[Any], signer: Signer) -> str:
77
+ stamp = int(time.time() * 1000)
78
+ return f"0x{contract}-{method}-{stamp}"
79
+
80
+
81
+ class TronContractAdapter(ContractAdapter):
82
+ """
83
+ TRON 区块链合约适配器
84
+
85
+ 使用 tronpy 库与 TRON 区块链交互。
86
+
87
+ Args:
88
+ rpc_url: TRON RPC 节点地址
89
+ identity_registry: IdentityRegistry 合约地址
90
+ validation_registry: ValidationRegistry 合约地址
91
+ reputation_registry: ReputationRegistry 合约地址
92
+ fee_limit: 交易费用上限(单位:sun)
93
+ retry_config: 重试配置
94
+
95
+ Example:
96
+ >>> adapter = TronContractAdapter(
97
+ ... rpc_url="https://nile.trongrid.io",
98
+ ... identity_registry="TIdentity...",
99
+ ... fee_limit=10_000_000,
100
+ ... )
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ rpc_url: str,
106
+ identity_registry: Optional[str],
107
+ validation_registry: Optional[str],
108
+ reputation_registry: Optional[str],
109
+ identity_registry_abi_path: Optional[str] = None,
110
+ validation_registry_abi_path: Optional[str] = None,
111
+ reputation_registry_abi_path: Optional[str] = None,
112
+ fee_limit: Optional[int] = None,
113
+ retry_config: Optional[RetryConfig] = None,
114
+ ) -> None:
115
+ self.rpc_url = rpc_url
116
+ self.identity_registry = identity_registry
117
+ self.validation_registry = validation_registry
118
+ self.reputation_registry = reputation_registry
119
+ self.identity_registry_abi_path = identity_registry_abi_path
120
+ self.validation_registry_abi_path = validation_registry_abi_path
121
+ self.reputation_registry_abi_path = reputation_registry_abi_path
122
+ self.fee_limit = fee_limit or 10_000_000
123
+ self.retry_config = retry_config or DEFAULT_RETRY_CONFIG
124
+ self._client = None
125
+
126
+ def _get_client(self):
127
+ """获取或创建 TRON 客户端"""
128
+ if self._client is None:
129
+ try:
130
+ from tronpy import Tron
131
+ from tronpy.providers import HTTPProvider
132
+ except ImportError as exc:
133
+ raise RuntimeError("tronpy is required for TronContractAdapter") from exc
134
+ self._client = Tron(provider=HTTPProvider(self.rpc_url))
135
+ if self.fee_limit:
136
+ self._client.conf["fee_limit"] = self.fee_limit
137
+ return self._client
138
+
139
+ def _resolve_contract(self, contract: str):
140
+ """解析合约地址并获取合约引用"""
141
+ address = None
142
+ abi_path = None
143
+ if contract == "identity":
144
+ address = self.identity_registry
145
+ abi_path = self.identity_registry_abi_path
146
+ elif contract == "validation":
147
+ address = self.validation_registry
148
+ abi_path = self.validation_registry_abi_path
149
+ elif contract == "reputation":
150
+ address = self.reputation_registry
151
+ abi_path = self.reputation_registry_abi_path
152
+
153
+ if not address:
154
+ raise MissingContractAddressError(contract)
155
+
156
+ client = self._get_client()
157
+ try:
158
+ contract_ref = client.get_contract(address)
159
+
160
+ # 如果提供了 ABI 文件路径,使用文件中的 ABI(支持 ABIEncoderV2)
161
+ if abi_path:
162
+ import json
163
+ with open(abi_path) as f:
164
+ abi_data = json.load(f)
165
+ if isinstance(abi_data, dict) and "abi" in abi_data:
166
+ contract_ref.abi = abi_data["abi"]
167
+ elif isinstance(abi_data, list):
168
+ contract_ref.abi = abi_data
169
+ logger.debug("Loaded ABI from %s for %s", abi_path, contract)
170
+ else:
171
+ # 没有提供 ABI 文件,尝试修复 ABIEncoderV2 的 tuple 类型
172
+ # tronpy 不支持 components 字段,需要手动展开
173
+ fixed_abi = self._fix_abi_encoder_v2(contract_ref.abi)
174
+ if fixed_abi:
175
+ contract_ref.abi = fixed_abi
176
+ logger.debug("Fixed ABIEncoderV2 for %s", contract)
177
+ except Exception as e:
178
+ raise ContractCallError(contract, "get_contract", str(e)) from e
179
+
180
+ return contract_ref
181
+
182
+ def _fix_abi_encoder_v2(self, abi: list) -> list:
183
+ """
184
+ 修复 ABIEncoderV2 的 tuple 类型
185
+
186
+ tronpy 不支持 components 字段,需要将 tuple 类型展开为基本类型
187
+ 注意:链上返回的 type 可能是 "Function" 而不是 "function"
188
+ """
189
+ if not abi:
190
+ return abi
191
+
192
+ def expand_type(item: dict) -> str:
193
+ """展开 tuple 类型为 (type1,type2,...) 格式"""
194
+ t = item.get("type", "")
195
+ if t == "tuple" or t.startswith("tuple["):
196
+ components = item.get("components", [])
197
+ if components:
198
+ inner = ",".join(expand_type(c) for c in components)
199
+ if t == "tuple":
200
+ return f"({inner})"
201
+ else:
202
+ # tuple[] -> (...)[]
203
+ suffix = t[5:] # 获取 [] 部分
204
+ return f"({inner}){suffix}"
205
+ return t
206
+
207
+ fixed = []
208
+ for entry in abi:
209
+ # 使用 .lower() 进行大小写不敏感比较
210
+ if entry.get("type", "").lower() != "function":
211
+ fixed.append(entry)
212
+ continue
213
+
214
+ new_entry = dict(entry)
215
+
216
+ # 修复 inputs
217
+ if "inputs" in entry:
218
+ new_inputs = []
219
+ for inp in entry["inputs"]:
220
+ new_inp = dict(inp)
221
+ new_inp["type"] = expand_type(inp)
222
+ # 移除 components 字段,tronpy 不需要
223
+ new_inp.pop("components", None)
224
+ new_inputs.append(new_inp)
225
+ new_entry["inputs"] = new_inputs
226
+
227
+ # 修复 outputs
228
+ if "outputs" in entry:
229
+ new_outputs = []
230
+ for out in entry["outputs"]:
231
+ new_out = dict(out)
232
+ new_out["type"] = expand_type(out)
233
+ new_out.pop("components", None)
234
+ new_outputs.append(new_out)
235
+ new_entry["outputs"] = new_outputs
236
+
237
+ fixed.append(new_entry)
238
+
239
+ return fixed
240
+
241
+ @staticmethod
242
+ def _pick_function(contract_ref, method: str, params: List[Any]):
243
+ """选择合约方法(处理重载)"""
244
+
245
+ def _get_overload(name: str, arity: int):
246
+ try:
247
+ from tronpy.contract import ContractMethod
248
+ except ImportError as exc:
249
+ raise RuntimeError("tronpy is required") from exc
250
+ for item in contract_ref.abi:
251
+ if item.get("type", "").lower() != "function":
252
+ continue
253
+ if item.get("name") != name:
254
+ continue
255
+ inputs = item.get("inputs", [])
256
+ if len(inputs) == arity:
257
+ return ContractMethod(item, contract_ref)
258
+ raise ContractFunctionNotFoundError(
259
+ contract_ref.contract_address, name, arity
260
+ )
261
+
262
+ def _get(name: str):
263
+ return getattr(contract_ref.functions, name)
264
+
265
+ # 处理 register 方法的重载
266
+ if method == "register" and "(" not in method:
267
+ if len(params) == 0:
268
+ try:
269
+ return _get_overload("register", 0)
270
+ except Exception:
271
+ pass
272
+ elif len(params) == 1:
273
+ try:
274
+ return _get_overload("register", 1)
275
+ except Exception:
276
+ pass
277
+ elif len(params) == 2:
278
+ try:
279
+ return _get_overload("register", 2)
280
+ except Exception:
281
+ pass
282
+ try:
283
+ logger.debug("register params=%s try_function=%s", params, method)
284
+ return _get(method)
285
+ except Exception:
286
+ pass
287
+ raise ContractFunctionNotFoundError(
288
+ contract_ref.contract_address, "register"
289
+ )
290
+
291
+ try:
292
+ return _get(method)
293
+ except Exception:
294
+ pass
295
+ raise ContractFunctionNotFoundError(contract_ref.contract_address, method)
296
+
297
+ def call(self, contract: str, method: str, params: List[Any]) -> Any:
298
+ """调用合约只读方法"""
299
+ contract_ref = self._resolve_contract(contract)
300
+ function = self._pick_function(contract_ref, method, params)
301
+ try:
302
+ result = function(*params)
303
+ # tronpy 的 ContractMethod 在某些情况下直接返回结果
304
+ # 而不是返回一个需要 .call() 的对象
305
+ if hasattr(result, 'call'):
306
+ return result.call()
307
+ return result
308
+ except Exception as e:
309
+ raise ContractCallError(contract, method, str(e)) from e
310
+
311
+ def send(self, contract: str, method: str, params: List[Any], signer: Signer) -> str:
312
+ """
313
+ 发送合约交易(带重试)
314
+
315
+ Args:
316
+ contract: 合约名称
317
+ method: 方法名
318
+ params: 参数列表
319
+ signer: 签名器
320
+
321
+ Returns:
322
+ 交易 ID
323
+
324
+ Raises:
325
+ ContractCallError: 合约调用失败
326
+ TransactionFailedError: 交易执行失败
327
+ InsufficientEnergyError: 能量不足
328
+ """
329
+ return self._send_with_retry(contract, method, params, signer)
330
+
331
+ @retry(operation_name="contract_send")
332
+ def _send_with_retry(
333
+ self, contract: str, method: str, params: List[Any], signer: Signer
334
+ ) -> str:
335
+ """带重试的交易发送"""
336
+ contract_ref = self._resolve_contract(contract)
337
+
338
+ # 检查能量(仅 register 方法)
339
+ if method == "register":
340
+ self._check_energy(signer)
341
+
342
+ function = self._pick_function(contract_ref, method, params)
343
+ logger.debug(
344
+ "Sending tx: contract=%s, method=%s, params_count=%d",
345
+ contract,
346
+ method,
347
+ len(params),
348
+ )
349
+
350
+ try:
351
+ # 尝试使用标准方式构建交易
352
+ try:
353
+ txn = function(*params).with_owner(signer.get_address()).build()
354
+ except ValueError as ve:
355
+ if "ABIEncoderV2" in str(ve):
356
+ # ABIEncoderV2 需要手动编码参数
357
+ txn = self._build_tx_with_abi_encoder_v2(
358
+ contract_ref, method, params, signer
359
+ )
360
+ else:
361
+ raise
362
+
363
+ signed = signer.sign_tx(txn)
364
+ result = signed.broadcast().wait()
365
+
366
+ tx_id = result.get("id")
367
+ if not tx_id:
368
+ raise TransactionFailedError(reason="No transaction ID in result")
369
+
370
+ logger.info("Transaction sent: %s", tx_id)
371
+ return tx_id
372
+
373
+ except Exception as e:
374
+ error_msg = str(e).lower()
375
+ if "energy" in error_msg or "bandwidth" in error_msg:
376
+ raise InsufficientEnergyError() from e
377
+ if "revert" in error_msg:
378
+ raise TransactionFailedError(reason=str(e)) from e
379
+ # 网络错误可重试
380
+ if any(
381
+ kw in error_msg
382
+ for kw in ["timeout", "connection", "network", "unavailable"]
383
+ ):
384
+ raise NetworkError(str(e)) from e
385
+ raise ContractCallError(contract, method, str(e)) from e
386
+
387
+ def _build_tx_with_abi_encoder_v2(
388
+ self, contract_ref, method: str, params: List[Any], signer: Signer
389
+ ):
390
+ """
391
+ 使用 eth_abi 手动编码 ABIEncoderV2 参数
392
+
393
+ tronpy 不支持 ABIEncoderV2 的 tuple 类型,需要手动编码参数并构建交易
394
+ """
395
+ try:
396
+ from eth_abi import encode
397
+ from eth_utils import keccak
398
+ except ImportError:
399
+ raise RuntimeError("eth_abi and eth_utils are required for ABIEncoderV2 encoding")
400
+
401
+ # 找到方法的 ABI(支持重载方法)
402
+ # 注意:链上返回的 type 可能是 "Function" 而不是 "function"
403
+ method_abi = None
404
+ for item in contract_ref.abi:
405
+ if item.get("type", "").lower() == "function" and item.get("name") == method:
406
+ inputs = item.get("inputs", [])
407
+ if len(inputs) == len(params):
408
+ method_abi = item
409
+ break
410
+
411
+ if not method_abi:
412
+ # 打印调试信息
413
+ logger.debug(
414
+ "Looking for method %s with %d params in ABI with %d entries",
415
+ method, len(params), len(contract_ref.abi)
416
+ )
417
+ for item in contract_ref.abi:
418
+ if item.get("type", "").lower() == "function":
419
+ logger.debug(
420
+ " Found function: %s with %d inputs",
421
+ item.get("name"), len(item.get("inputs", []))
422
+ )
423
+ raise ContractFunctionNotFoundError(
424
+ contract_ref.contract_address, method, len(params)
425
+ )
426
+
427
+ # 构建类型签名
428
+ def get_type_str(inp: dict) -> str:
429
+ t = inp.get("type", "")
430
+ if t == "tuple" or t.startswith("tuple"):
431
+ components = inp.get("components", [])
432
+ inner = ",".join(get_type_str(c) for c in components)
433
+ if t == "tuple":
434
+ return f"({inner})"
435
+ else:
436
+ suffix = t[5:]
437
+ return f"({inner}){suffix}"
438
+ return t
439
+
440
+ types = [get_type_str(inp) for inp in method_abi.get("inputs", [])]
441
+ logger.debug("ABIEncoderV2 types: %s", types)
442
+
443
+ # 编码参数
444
+ encoded_params = encode(types, params)
445
+
446
+ # 计算函数选择器 (keccak256 of function signature)
447
+ sig = f"{method}({','.join(types)})"
448
+ selector = keccak(text=sig)[:4]
449
+ logger.debug("Function signature: %s, selector: %s", sig, selector.hex())
450
+
451
+ # 构建完整的 calldata
452
+ data = selector + encoded_params
453
+
454
+ # 使用 tronpy 的底层 API 构建交易
455
+ client = self._get_client()
456
+ owner_address = signer.get_address()
457
+
458
+ # 构建 TriggerSmartContract 交易
459
+ txn = client.trx._build_transaction(
460
+ "TriggerSmartContract",
461
+ {
462
+ "owner_address": owner_address,
463
+ "contract_address": contract_ref.contract_address,
464
+ "data": data.hex(),
465
+ },
466
+ method=method,
467
+ )
468
+
469
+ return txn
470
+
471
+ def _check_energy(self, signer: Signer) -> None:
472
+ """检查账户能量"""
473
+ try:
474
+ client = self._get_client()
475
+ address = signer.get_address()
476
+ resource = client.get_account_resource(address)
477
+ energy_limit = resource.get("EnergyLimit", 0)
478
+ energy_used = resource.get("EnergyUsed", 0)
479
+ energy_left = max(energy_limit - energy_used, 0)
480
+ logger.debug(
481
+ "Energy check: left=%d, limit=%d, used=%d",
482
+ energy_left,
483
+ energy_limit,
484
+ energy_used,
485
+ )
486
+ if energy_left < 100_000:
487
+ logger.warning("Low energy: %d", energy_left)
488
+ except Exception as e:
489
+ logger.warning("Energy check failed: %s", e)