agent0-sdk 0.31__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.
- agent0_sdk/__init__.py +52 -0
- agent0_sdk/core/agent.py +992 -0
- agent0_sdk/core/contracts.py +497 -0
- agent0_sdk/core/endpoint_crawler.py +330 -0
- agent0_sdk/core/feedback_manager.py +1023 -0
- agent0_sdk/core/indexer.py +1754 -0
- agent0_sdk/core/ipfs_client.py +355 -0
- agent0_sdk/core/models.py +313 -0
- agent0_sdk/core/oasf_validator.py +98 -0
- agent0_sdk/core/sdk.py +1045 -0
- agent0_sdk/core/subgraph_client.py +833 -0
- agent0_sdk/core/web3_client.py +192 -0
- agent0_sdk/taxonomies/all_domains.json +1565 -0
- agent0_sdk/taxonomies/all_skills.json +1030 -0
- agent0_sdk-0.31.dist-info/METADATA +367 -0
- agent0_sdk-0.31.dist-info/RECORD +33 -0
- agent0_sdk-0.31.dist-info/WHEEL +5 -0
- agent0_sdk-0.31.dist-info/licenses/LICENSE +22 -0
- agent0_sdk-0.31.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/config.py +46 -0
- tests/conftest.py +22 -0
- tests/discover_test_data.py +445 -0
- tests/test_feedback.py +417 -0
- tests/test_models.py +224 -0
- tests/test_multi_chain.py +588 -0
- tests/test_oasf_management.py +404 -0
- tests/test_real_public_servers.py +103 -0
- tests/test_registration.py +267 -0
- tests/test_registrationIpfs.py +227 -0
- tests/test_sdk.py +240 -0
- tests/test_search.py +415 -0
- tests/test_transfer.py +255 -0
agent0_sdk/core/agent.py
ADDED
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent class for managing individual agents.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
from .models import (
|
|
14
|
+
AgentId, Address, URI, Timestamp, IdemKey,
|
|
15
|
+
EndpointType, TrustModel, Endpoint, RegistrationFile
|
|
16
|
+
)
|
|
17
|
+
from .web3_client import Web3Client
|
|
18
|
+
from .endpoint_crawler import EndpointCrawler
|
|
19
|
+
from .oasf_validator import validate_skill, validate_domain
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .sdk import SDK
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Agent:
|
|
28
|
+
"""Represents an individual agent with its registration data."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, sdk: "SDK", registration_file: RegistrationFile):
|
|
31
|
+
"""Initialize agent with SDK and registration file."""
|
|
32
|
+
self.sdk = sdk
|
|
33
|
+
self.registration_file = registration_file
|
|
34
|
+
# Track which metadata has changed since last registration to avoid sending unchanged data
|
|
35
|
+
self._dirty_metadata = set()
|
|
36
|
+
self._last_registered_wallet = None
|
|
37
|
+
self._last_registered_ens = None
|
|
38
|
+
# Initialize endpoint crawler for fetching capabilities
|
|
39
|
+
self._endpoint_crawler = EndpointCrawler(timeout=5)
|
|
40
|
+
|
|
41
|
+
# Read-only properties for direct access
|
|
42
|
+
@property
|
|
43
|
+
def agentId(self) -> Optional[AgentId]:
|
|
44
|
+
"""Get agent ID (read-only)."""
|
|
45
|
+
return self.registration_file.agentId
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def agentURI(self) -> Optional[URI]:
|
|
49
|
+
"""Get agent URI (read-only)."""
|
|
50
|
+
return self.registration_file.agentURI
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def name(self) -> str:
|
|
54
|
+
"""Get agent name (read-only)."""
|
|
55
|
+
return self.registration_file.name
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def description(self) -> str:
|
|
59
|
+
"""Get agent description (read-only)."""
|
|
60
|
+
return self.registration_file.description
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def image(self) -> Optional[URI]:
|
|
64
|
+
"""Get agent image URI (read-only)."""
|
|
65
|
+
return self.registration_file.image
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def active(self) -> bool:
|
|
69
|
+
"""Get agent active status (read-only)."""
|
|
70
|
+
return self.registration_file.active
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def x402support(self) -> bool:
|
|
74
|
+
"""Get agent x402 support status (read-only)."""
|
|
75
|
+
return self.registration_file.x402support
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def walletAddress(self) -> Optional[Address]:
|
|
79
|
+
"""Get agent wallet address (read-only)."""
|
|
80
|
+
return self.registration_file.walletAddress
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def walletChainId(self) -> Optional[int]:
|
|
84
|
+
"""Get agent wallet chain ID (read-only)."""
|
|
85
|
+
return self.registration_file.walletChainId
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def endpoints(self) -> List[Endpoint]:
|
|
89
|
+
"""Get agent endpoints list (read-only - use setter methods to modify)."""
|
|
90
|
+
return self.registration_file.endpoints
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def trustModels(self) -> List[Union[TrustModel, str]]:
|
|
94
|
+
"""Get agent trust models list (read-only - use setter methods to modify)."""
|
|
95
|
+
return self.registration_file.trustModels
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def metadata(self) -> Dict[str, Any]:
|
|
99
|
+
"""Get agent metadata dict (read-only - use setter methods to modify)."""
|
|
100
|
+
return self.registration_file.metadata
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def updatedAt(self) -> Timestamp:
|
|
104
|
+
"""Get last update timestamp (read-only)."""
|
|
105
|
+
return self.registration_file.updatedAt
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def owners(self) -> List[Address]:
|
|
109
|
+
"""Get agent owners list (read-only)."""
|
|
110
|
+
return self.registration_file.owners
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def operators(self) -> List[Address]:
|
|
114
|
+
"""Get agent operators list (read-only)."""
|
|
115
|
+
return self.registration_file.operators
|
|
116
|
+
|
|
117
|
+
# Derived endpoint properties (convenience)
|
|
118
|
+
@property
|
|
119
|
+
def mcpEndpoint(self) -> Optional[str]:
|
|
120
|
+
"""Get MCP endpoint value (read-only)."""
|
|
121
|
+
for endpoint in self.registration_file.endpoints:
|
|
122
|
+
if endpoint.type == EndpointType.MCP:
|
|
123
|
+
return endpoint.value
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def a2aEndpoint(self) -> Optional[str]:
|
|
128
|
+
"""Get A2A endpoint value (read-only)."""
|
|
129
|
+
for endpoint in self.registration_file.endpoints:
|
|
130
|
+
if endpoint.type == EndpointType.A2A:
|
|
131
|
+
return endpoint.value
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def ensEndpoint(self) -> Optional[str]:
|
|
136
|
+
"""Get ENS endpoint value (read-only)."""
|
|
137
|
+
for endpoint in self.registration_file.endpoints:
|
|
138
|
+
if endpoint.type == EndpointType.ENS:
|
|
139
|
+
return endpoint.value
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def mcpTools(self) -> Optional[List[str]]:
|
|
144
|
+
"""Get MCP tools list (read-only)."""
|
|
145
|
+
for endpoint in self.registration_file.endpoints:
|
|
146
|
+
if endpoint.type == EndpointType.MCP:
|
|
147
|
+
return endpoint.meta.get('mcpTools')
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def mcpPrompts(self) -> Optional[List[str]]:
|
|
152
|
+
"""Get MCP prompts list (read-only)."""
|
|
153
|
+
for endpoint in self.registration_file.endpoints:
|
|
154
|
+
if endpoint.type == EndpointType.MCP:
|
|
155
|
+
return endpoint.meta.get('mcpPrompts')
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def mcpResources(self) -> Optional[List[str]]:
|
|
160
|
+
"""Get MCP resources list (read-only)."""
|
|
161
|
+
for endpoint in self.registration_file.endpoints:
|
|
162
|
+
if endpoint.type == EndpointType.MCP:
|
|
163
|
+
return endpoint.meta.get('mcpResources')
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def a2aSkills(self) -> Optional[List[str]]:
|
|
168
|
+
"""Get A2A skills list (read-only)."""
|
|
169
|
+
for endpoint in self.registration_file.endpoints:
|
|
170
|
+
if endpoint.type == EndpointType.A2A:
|
|
171
|
+
return endpoint.meta.get('a2aSkills')
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def registrationFile(self) -> RegistrationFile:
|
|
175
|
+
"""Get the compiled registration file."""
|
|
176
|
+
return self.registration_file
|
|
177
|
+
|
|
178
|
+
def _collectMetadataForRegistration(self) -> List[Dict[str, Any]]:
|
|
179
|
+
"""Collect all metadata entries for registration."""
|
|
180
|
+
metadata_entries = []
|
|
181
|
+
|
|
182
|
+
# Add wallet address metadata
|
|
183
|
+
if self.walletAddress:
|
|
184
|
+
addr_bytes = bytes.fromhex(self.walletAddress[2:]) # Remove '0x' prefix
|
|
185
|
+
metadata_entries.append({
|
|
186
|
+
"key": "agentWallet",
|
|
187
|
+
"value": addr_bytes
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
# Add ENS name metadata
|
|
191
|
+
if self.ensEndpoint:
|
|
192
|
+
name_bytes = self.ensEndpoint.encode('utf-8')
|
|
193
|
+
metadata_entries.append({
|
|
194
|
+
"key": "agentName",
|
|
195
|
+
"value": name_bytes
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
# Add custom metadata
|
|
199
|
+
for key, value in self.metadata.items():
|
|
200
|
+
if isinstance(value, str):
|
|
201
|
+
value_bytes = value.encode('utf-8')
|
|
202
|
+
elif isinstance(value, (int, float)):
|
|
203
|
+
value_bytes = str(value).encode('utf-8')
|
|
204
|
+
else:
|
|
205
|
+
value_bytes = str(value).encode('utf-8')
|
|
206
|
+
|
|
207
|
+
metadata_entries.append({
|
|
208
|
+
"key": key,
|
|
209
|
+
"value": value_bytes
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return metadata_entries
|
|
213
|
+
|
|
214
|
+
# Endpoint management
|
|
215
|
+
def setMCP(self, endpoint: str, version: str = "2025-06-18", auto_fetch: bool = True) -> 'Agent':
|
|
216
|
+
"""
|
|
217
|
+
Set MCP endpoint with version.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
endpoint: MCP endpoint URL
|
|
221
|
+
version: MCP version
|
|
222
|
+
auto_fetch: If True, automatically fetch capabilities from the endpoint (default: True)
|
|
223
|
+
"""
|
|
224
|
+
# Remove existing MCP endpoint if any
|
|
225
|
+
self.registration_file.endpoints = [
|
|
226
|
+
ep for ep in self.registration_file.endpoints
|
|
227
|
+
if ep.type != EndpointType.MCP
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
# Try to fetch capabilities from the endpoint (soft fail)
|
|
231
|
+
meta = {"version": version}
|
|
232
|
+
if auto_fetch:
|
|
233
|
+
try:
|
|
234
|
+
capabilities = self._endpoint_crawler.fetch_mcp_capabilities(endpoint)
|
|
235
|
+
if capabilities:
|
|
236
|
+
meta.update(capabilities)
|
|
237
|
+
logger.debug(
|
|
238
|
+
f"Fetched MCP capabilities: {len(capabilities.get('mcpTools', []))} tools, "
|
|
239
|
+
f"{len(capabilities.get('mcpPrompts', []))} prompts, "
|
|
240
|
+
f"{len(capabilities.get('mcpResources', []))} resources"
|
|
241
|
+
)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
# Soft fail - continue without capabilities
|
|
244
|
+
logger.debug(f"Could not fetch MCP capabilities (non-blocking): {e}")
|
|
245
|
+
|
|
246
|
+
# Add new MCP endpoint
|
|
247
|
+
mcp_endpoint = Endpoint(
|
|
248
|
+
type=EndpointType.MCP,
|
|
249
|
+
value=endpoint,
|
|
250
|
+
meta=meta
|
|
251
|
+
)
|
|
252
|
+
self.registration_file.endpoints.append(mcp_endpoint)
|
|
253
|
+
self.registration_file.updatedAt = int(time.time())
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
def setA2A(self, agentcard: str, version: str = "0.30", auto_fetch: bool = True) -> 'Agent':
|
|
257
|
+
"""
|
|
258
|
+
Set A2A endpoint with version.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
agentcard: A2A endpoint URL
|
|
262
|
+
version: A2A version
|
|
263
|
+
auto_fetch: If True, automatically fetch skills from the endpoint (default: True)
|
|
264
|
+
"""
|
|
265
|
+
# Remove existing A2A endpoint if any
|
|
266
|
+
self.registration_file.endpoints = [
|
|
267
|
+
ep for ep in self.registration_file.endpoints
|
|
268
|
+
if ep.type != EndpointType.A2A
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
# Try to fetch capabilities from the endpoint (soft fail)
|
|
272
|
+
meta = {"version": version}
|
|
273
|
+
if auto_fetch:
|
|
274
|
+
try:
|
|
275
|
+
capabilities = self._endpoint_crawler.fetch_a2a_capabilities(agentcard)
|
|
276
|
+
if capabilities:
|
|
277
|
+
meta.update(capabilities)
|
|
278
|
+
skills_count = len(capabilities.get('a2aSkills', []))
|
|
279
|
+
logger.debug(f"Fetched A2A capabilities: {skills_count} skills")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
# Soft fail - continue without capabilities
|
|
282
|
+
logger.debug(f"Could not fetch A2A capabilities (non-blocking): {e}")
|
|
283
|
+
|
|
284
|
+
# Add new A2A endpoint
|
|
285
|
+
a2a_endpoint = Endpoint(
|
|
286
|
+
type=EndpointType.A2A,
|
|
287
|
+
value=agentcard,
|
|
288
|
+
meta=meta
|
|
289
|
+
)
|
|
290
|
+
self.registration_file.endpoints.append(a2a_endpoint)
|
|
291
|
+
self.registration_file.updatedAt = int(time.time())
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
def removeEndpoint(
|
|
295
|
+
self,
|
|
296
|
+
type: Optional[EndpointType] = None,
|
|
297
|
+
value: Optional[str] = None
|
|
298
|
+
) -> 'Agent':
|
|
299
|
+
"""Remove endpoint(s) with wildcard semantics."""
|
|
300
|
+
if type is None and value is None:
|
|
301
|
+
# Remove all endpoints
|
|
302
|
+
self.registration_file.endpoints.clear()
|
|
303
|
+
else:
|
|
304
|
+
# Remove matching endpoints
|
|
305
|
+
self.registration_file.endpoints = [
|
|
306
|
+
ep for ep in self.registration_file.endpoints
|
|
307
|
+
if not (
|
|
308
|
+
(type is None or ep.type == type) and
|
|
309
|
+
(value is None or ep.value == value)
|
|
310
|
+
)
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
self.registration_file.updatedAt = int(time.time())
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
def removeEndpoints(self) -> 'Agent':
|
|
317
|
+
"""Remove all endpoints."""
|
|
318
|
+
return self.removeEndpoint()
|
|
319
|
+
|
|
320
|
+
# OASF endpoint management
|
|
321
|
+
def _get_or_create_oasf_endpoint(self) -> Endpoint:
|
|
322
|
+
"""Get existing OASF endpoint or create a new one with default values."""
|
|
323
|
+
# Find existing OASF endpoint
|
|
324
|
+
for ep in self.registration_file.endpoints:
|
|
325
|
+
if ep.type == EndpointType.OASF:
|
|
326
|
+
return ep
|
|
327
|
+
|
|
328
|
+
# Create new OASF endpoint with default values
|
|
329
|
+
oasf_endpoint = Endpoint(
|
|
330
|
+
type=EndpointType.OASF,
|
|
331
|
+
value="https://github.com/agntcy/oasf/",
|
|
332
|
+
meta={"version": "v0.8.0", "skills": [], "domains": []}
|
|
333
|
+
)
|
|
334
|
+
self.registration_file.endpoints.append(oasf_endpoint)
|
|
335
|
+
return oasf_endpoint
|
|
336
|
+
|
|
337
|
+
def addSkill(self, slug: str, validate_oasf: bool = False) -> 'Agent':
|
|
338
|
+
"""
|
|
339
|
+
Add a skill to the OASF endpoint.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
slug: The skill slug to add (e.g., "natural_language_processing/summarization")
|
|
343
|
+
validate_oasf: If True, validate the slug against the OASF taxonomy (default: False)
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
self for method chaining
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
ValueError: If validate_oasf=True and the slug is not valid
|
|
350
|
+
"""
|
|
351
|
+
if validate_oasf:
|
|
352
|
+
if not validate_skill(slug):
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"Invalid OASF skill slug: {slug}. "
|
|
355
|
+
"Use validate_oasf=False to skip validation."
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
oasf_endpoint = self._get_or_create_oasf_endpoint()
|
|
359
|
+
|
|
360
|
+
# Initialize skills array if missing
|
|
361
|
+
if "skills" not in oasf_endpoint.meta:
|
|
362
|
+
oasf_endpoint.meta["skills"] = []
|
|
363
|
+
|
|
364
|
+
# Add slug if not already present (avoid duplicates)
|
|
365
|
+
skills = oasf_endpoint.meta["skills"]
|
|
366
|
+
if slug not in skills:
|
|
367
|
+
skills.append(slug)
|
|
368
|
+
|
|
369
|
+
self.registration_file.updatedAt = int(time.time())
|
|
370
|
+
return self
|
|
371
|
+
|
|
372
|
+
def removeSkill(self, slug: str) -> 'Agent':
|
|
373
|
+
"""
|
|
374
|
+
Remove a skill from the OASF endpoint.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
slug: The skill slug to remove
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
self for method chaining
|
|
381
|
+
"""
|
|
382
|
+
# Find OASF endpoint
|
|
383
|
+
for ep in self.registration_file.endpoints:
|
|
384
|
+
if ep.type == EndpointType.OASF:
|
|
385
|
+
if "skills" in ep.meta and isinstance(ep.meta["skills"], list):
|
|
386
|
+
skills = ep.meta["skills"]
|
|
387
|
+
if slug in skills:
|
|
388
|
+
skills.remove(slug)
|
|
389
|
+
self.registration_file.updatedAt = int(time.time())
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
return self
|
|
393
|
+
|
|
394
|
+
def addDomain(self, slug: str, validate_oasf: bool = False) -> 'Agent':
|
|
395
|
+
"""
|
|
396
|
+
Add a domain to the OASF endpoint.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
slug: The domain slug to add (e.g., "finance_and_business/investment_services")
|
|
400
|
+
validate_oasf: If True, validate the slug against the OASF taxonomy (default: False)
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
self for method chaining
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ValueError: If validate_oasf=True and the slug is not valid
|
|
407
|
+
"""
|
|
408
|
+
if validate_oasf:
|
|
409
|
+
if not validate_domain(slug):
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"Invalid OASF domain slug: {slug}. "
|
|
412
|
+
"Use validate_oasf=False to skip validation."
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
oasf_endpoint = self._get_or_create_oasf_endpoint()
|
|
416
|
+
|
|
417
|
+
# Initialize domains array if missing
|
|
418
|
+
if "domains" not in oasf_endpoint.meta:
|
|
419
|
+
oasf_endpoint.meta["domains"] = []
|
|
420
|
+
|
|
421
|
+
# Add slug if not already present (avoid duplicates)
|
|
422
|
+
domains = oasf_endpoint.meta["domains"]
|
|
423
|
+
if slug not in domains:
|
|
424
|
+
domains.append(slug)
|
|
425
|
+
|
|
426
|
+
self.registration_file.updatedAt = int(time.time())
|
|
427
|
+
return self
|
|
428
|
+
|
|
429
|
+
def removeDomain(self, slug: str) -> 'Agent':
|
|
430
|
+
"""
|
|
431
|
+
Remove a domain from the OASF endpoint.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
slug: The domain slug to remove
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
self for method chaining
|
|
438
|
+
"""
|
|
439
|
+
# Find OASF endpoint
|
|
440
|
+
for ep in self.registration_file.endpoints:
|
|
441
|
+
if ep.type == EndpointType.OASF:
|
|
442
|
+
if "domains" in ep.meta and isinstance(ep.meta["domains"], list):
|
|
443
|
+
domains = ep.meta["domains"]
|
|
444
|
+
if slug in domains:
|
|
445
|
+
domains.remove(slug)
|
|
446
|
+
self.registration_file.updatedAt = int(time.time())
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
return self
|
|
450
|
+
|
|
451
|
+
# Trust models
|
|
452
|
+
def setTrust(
|
|
453
|
+
self,
|
|
454
|
+
reputation: bool = False,
|
|
455
|
+
cryptoEconomic: bool = False,
|
|
456
|
+
teeAttestation: bool = False
|
|
457
|
+
) -> 'Agent':
|
|
458
|
+
"""Set trust models using keyword arguments."""
|
|
459
|
+
trust_models = []
|
|
460
|
+
if reputation:
|
|
461
|
+
trust_models.append(TrustModel.REPUTATION)
|
|
462
|
+
if cryptoEconomic:
|
|
463
|
+
trust_models.append(TrustModel.CRYPTO_ECONOMIC)
|
|
464
|
+
if teeAttestation:
|
|
465
|
+
trust_models.append(TrustModel.TEE_ATTESTATION)
|
|
466
|
+
|
|
467
|
+
self.registration_file.trustModels = trust_models
|
|
468
|
+
self.registration_file.updatedAt = int(time.time())
|
|
469
|
+
return self
|
|
470
|
+
|
|
471
|
+
def trustModels(self, models: List[Union[TrustModel, str]]) -> 'Agent':
|
|
472
|
+
"""Set trust models (replace set)."""
|
|
473
|
+
self.registration_file.trustModels = models
|
|
474
|
+
self.registration_file.updatedAt = int(time.time())
|
|
475
|
+
return self
|
|
476
|
+
|
|
477
|
+
# Basic info
|
|
478
|
+
def updateInfo(
|
|
479
|
+
self,
|
|
480
|
+
name: Optional[str] = None,
|
|
481
|
+
description: Optional[str] = None,
|
|
482
|
+
image: Optional[URI] = None
|
|
483
|
+
) -> 'Agent':
|
|
484
|
+
"""Update basic agent information."""
|
|
485
|
+
if name is not None:
|
|
486
|
+
self.registration_file.name = name
|
|
487
|
+
if description is not None:
|
|
488
|
+
self.registration_file.description = description
|
|
489
|
+
if image is not None:
|
|
490
|
+
self.registration_file.image = image
|
|
491
|
+
|
|
492
|
+
self.registration_file.updatedAt = int(time.time())
|
|
493
|
+
return self
|
|
494
|
+
|
|
495
|
+
def setAgentWallet(self, addr: Optional[Address], chainId: Optional[int] = None) -> 'Agent':
|
|
496
|
+
"""Set agent wallet address in registration file (will be saved on-chain during next register call)."""
|
|
497
|
+
# Validate address format if provided
|
|
498
|
+
if addr:
|
|
499
|
+
if not addr.startswith("0x") or len(addr) != 42:
|
|
500
|
+
raise ValueError(f"Invalid Ethereum address format: {addr}. Must be 42 characters starting with '0x'")
|
|
501
|
+
|
|
502
|
+
# Validate hexadecimal characters
|
|
503
|
+
try:
|
|
504
|
+
int(addr[2:], 16)
|
|
505
|
+
except ValueError:
|
|
506
|
+
raise ValueError(f"Invalid hexadecimal characters in address: {addr}")
|
|
507
|
+
|
|
508
|
+
# Determine chain ID to use
|
|
509
|
+
if chainId is None:
|
|
510
|
+
# Extract chain ID from agentId if available, otherwise use SDK's chain ID
|
|
511
|
+
if self.agentId and ":" in self.agentId:
|
|
512
|
+
try:
|
|
513
|
+
chainId = int(self.agentId.split(":")[0]) # First part is chainId
|
|
514
|
+
except (ValueError, IndexError):
|
|
515
|
+
chainId = self.sdk.chainId # Use SDK's chain ID as fallback
|
|
516
|
+
else:
|
|
517
|
+
chainId = self.sdk.chainId # Use SDK's chain ID as fallback
|
|
518
|
+
|
|
519
|
+
# Check if wallet changed
|
|
520
|
+
if addr != self._last_registered_wallet:
|
|
521
|
+
self._dirty_metadata.add("agentWallet")
|
|
522
|
+
|
|
523
|
+
# Update local registration file
|
|
524
|
+
self.registration_file.walletAddress = addr
|
|
525
|
+
self.registration_file.walletChainId = chainId
|
|
526
|
+
self.registration_file.updatedAt = int(time.time())
|
|
527
|
+
|
|
528
|
+
return self
|
|
529
|
+
|
|
530
|
+
def setENS(self, name: str, version: str = "1.0") -> 'Agent':
|
|
531
|
+
"""Set ENS name both on-chain and in registration file."""
|
|
532
|
+
# Remove existing ENS endpoints
|
|
533
|
+
self.registration_file.endpoints = [
|
|
534
|
+
ep for ep in self.registration_file.endpoints
|
|
535
|
+
if ep.type != EndpointType.ENS
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
# Check if ENS changed
|
|
539
|
+
if name != self._last_registered_ens:
|
|
540
|
+
self._dirty_metadata.add("agentName")
|
|
541
|
+
|
|
542
|
+
# Add new ENS endpoint
|
|
543
|
+
ens_endpoint = Endpoint(
|
|
544
|
+
type=EndpointType.ENS,
|
|
545
|
+
value=name,
|
|
546
|
+
meta={"version": version}
|
|
547
|
+
)
|
|
548
|
+
self.registration_file.endpoints.append(ens_endpoint)
|
|
549
|
+
self.registration_file.updatedAt = int(time.time())
|
|
550
|
+
|
|
551
|
+
return self
|
|
552
|
+
|
|
553
|
+
def setActive(self, active: bool) -> 'Agent':
|
|
554
|
+
"""Set agent active status."""
|
|
555
|
+
self.registration_file.active = active
|
|
556
|
+
self.registration_file.updatedAt = int(time.time())
|
|
557
|
+
return self
|
|
558
|
+
|
|
559
|
+
def setX402Support(self, x402Support: bool) -> 'Agent':
|
|
560
|
+
"""Set agent x402 payment support."""
|
|
561
|
+
self.registration_file.x402support = x402Support
|
|
562
|
+
self.registration_file.updatedAt = int(time.time())
|
|
563
|
+
return self
|
|
564
|
+
|
|
565
|
+
# Metadata management
|
|
566
|
+
def setMetadata(self, kv: Dict[str, Any]) -> 'Agent':
|
|
567
|
+
"""Set metadata (SDK-managed bag)."""
|
|
568
|
+
# Mark all provided keys as dirty
|
|
569
|
+
for key in kv.keys():
|
|
570
|
+
self._dirty_metadata.add(key)
|
|
571
|
+
|
|
572
|
+
self.registration_file.metadata.update(kv)
|
|
573
|
+
self.registration_file.updatedAt = int(time.time())
|
|
574
|
+
return self
|
|
575
|
+
|
|
576
|
+
def getMetadata(self) -> Dict[str, Any]:
|
|
577
|
+
"""Get metadata."""
|
|
578
|
+
return self.registration_file.metadata.copy()
|
|
579
|
+
|
|
580
|
+
def delMetadata(self, key: str) -> 'Agent':
|
|
581
|
+
"""Delete a metadata key."""
|
|
582
|
+
if key in self.registration_file.metadata:
|
|
583
|
+
del self.registration_file.metadata[key]
|
|
584
|
+
# Mark this key as dirty for tracking
|
|
585
|
+
self._dirty_metadata.discard(key) # Remove from dirty set since it's being deleted
|
|
586
|
+
self.registration_file.updatedAt = int(time.time())
|
|
587
|
+
return self
|
|
588
|
+
|
|
589
|
+
# Local inspection
|
|
590
|
+
def getRegistrationFile(self) -> RegistrationFile:
|
|
591
|
+
"""Get current in-memory file (not necessarily published yet)."""
|
|
592
|
+
return self.registration_file
|
|
593
|
+
|
|
594
|
+
# Registration (on-chain)
|
|
595
|
+
def registerIPFS(self) -> RegistrationFile:
|
|
596
|
+
"""Register agent on-chain with IPFS flow (mint -> pin -> set URI) or update existing registration."""
|
|
597
|
+
# Validate basic info
|
|
598
|
+
if not self.registration_file.name or not self.registration_file.description:
|
|
599
|
+
raise ValueError("Agent must have name and description before registration")
|
|
600
|
+
|
|
601
|
+
if self.registration_file.agentId:
|
|
602
|
+
# Agent already registered - update registration file and redeploy
|
|
603
|
+
logger.debug("Agent already registered, updating registration file")
|
|
604
|
+
|
|
605
|
+
# Upload updated registration file to IPFS
|
|
606
|
+
ipfsCid = self.sdk.ipfs_client.addRegistrationFile(
|
|
607
|
+
self.registration_file,
|
|
608
|
+
chainId=self.sdk.chain_id(),
|
|
609
|
+
identityRegistryAddress=self.sdk.identity_registry.address
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Update metadata on-chain if agent is already registered
|
|
613
|
+
# Only send transactions for dirty (changed) metadata to save gas
|
|
614
|
+
if self._dirty_metadata:
|
|
615
|
+
metadata_entries = self._collectMetadataForRegistration()
|
|
616
|
+
agentId = int(self.agentId.split(":")[-1])
|
|
617
|
+
for entry in metadata_entries:
|
|
618
|
+
# Only send transaction if this metadata key is dirty
|
|
619
|
+
if entry["key"] in self._dirty_metadata:
|
|
620
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
621
|
+
self.sdk.identity_registry,
|
|
622
|
+
"setMetadata",
|
|
623
|
+
agentId,
|
|
624
|
+
entry["key"],
|
|
625
|
+
entry["value"]
|
|
626
|
+
)
|
|
627
|
+
try:
|
|
628
|
+
self.sdk.web3_client.wait_for_transaction(txHash, timeout=30)
|
|
629
|
+
except Exception as e:
|
|
630
|
+
logger.warning(f"Transaction timeout for {entry['key']}: {e}")
|
|
631
|
+
logger.debug(f"Updated metadata on-chain: {entry['key']}")
|
|
632
|
+
else:
|
|
633
|
+
logger.debug("No metadata changes detected, skipping metadata updates")
|
|
634
|
+
|
|
635
|
+
# Update agent URI on-chain
|
|
636
|
+
agentId = int(self.agentId.split(":")[-1])
|
|
637
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
638
|
+
self.sdk.identity_registry,
|
|
639
|
+
"setAgentUri",
|
|
640
|
+
agentId,
|
|
641
|
+
f"ipfs://{ipfsCid}"
|
|
642
|
+
)
|
|
643
|
+
try:
|
|
644
|
+
self.sdk.web3_client.wait_for_transaction(txHash, timeout=30)
|
|
645
|
+
logger.debug(f"Updated agent URI on-chain: {txHash}")
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.warning(f"URI update timeout (transaction sent: {txHash}): {e}")
|
|
648
|
+
|
|
649
|
+
# Clear dirty flags after successful registration
|
|
650
|
+
self._last_registered_wallet = self.walletAddress
|
|
651
|
+
self._last_registered_ens = self.ensEndpoint
|
|
652
|
+
self._dirty_metadata.clear()
|
|
653
|
+
|
|
654
|
+
return self.registration_file
|
|
655
|
+
else:
|
|
656
|
+
# First time registration
|
|
657
|
+
logger.debug("Registering agent for the first time")
|
|
658
|
+
|
|
659
|
+
# Step 1: Register on-chain without URI
|
|
660
|
+
self._registerWithoutUri()
|
|
661
|
+
|
|
662
|
+
# Step 2: Prepare registration file with agent ID (already set by _registerWithoutUri)
|
|
663
|
+
# No need to modify agentId as it's already set correctly
|
|
664
|
+
|
|
665
|
+
# Step 3: Upload to IPFS
|
|
666
|
+
ipfsCid = self.sdk.ipfs_client.addRegistrationFile(
|
|
667
|
+
self.registration_file,
|
|
668
|
+
chainId=self.sdk.chain_id(),
|
|
669
|
+
identityRegistryAddress=self.sdk.identity_registry.address
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Step 4: Set agent URI on-chain
|
|
673
|
+
agentId = int(self.agentId.split(":")[-1])
|
|
674
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
675
|
+
self.sdk.identity_registry,
|
|
676
|
+
"setAgentUri",
|
|
677
|
+
agentId,
|
|
678
|
+
f"ipfs://{ipfsCid}"
|
|
679
|
+
)
|
|
680
|
+
try:
|
|
681
|
+
self.sdk.web3_client.wait_for_transaction(txHash, timeout=30)
|
|
682
|
+
logger.debug(f"Set agent URI on-chain: {txHash}")
|
|
683
|
+
except Exception as e:
|
|
684
|
+
logger.warning(f"URI set timeout (transaction sent: {txHash}): {e}")
|
|
685
|
+
|
|
686
|
+
# Clear dirty flags after successful registration
|
|
687
|
+
self._last_registered_wallet = self.walletAddress
|
|
688
|
+
self._last_registered_ens = self.ensEndpoint
|
|
689
|
+
self._dirty_metadata.clear()
|
|
690
|
+
|
|
691
|
+
return self.registration_file
|
|
692
|
+
|
|
693
|
+
def register(self, agentUri: str) -> RegistrationFile:
|
|
694
|
+
"""Register agent on-chain with direct URI or update existing registration."""
|
|
695
|
+
# Validate basic info
|
|
696
|
+
if not self.registration_file.name or not self.registration_file.description:
|
|
697
|
+
raise ValueError("Agent must have name and description before registration")
|
|
698
|
+
|
|
699
|
+
if self.registration_file.agentId:
|
|
700
|
+
# Agent already registered - update agent URI
|
|
701
|
+
logger.debug("Agent already registered, updating agent URI")
|
|
702
|
+
self.setAgentUri(agentUri)
|
|
703
|
+
return self.registration_file
|
|
704
|
+
else:
|
|
705
|
+
# First time registration
|
|
706
|
+
logger.debug("Registering agent for the first time")
|
|
707
|
+
return self._registerWithUri(agentUri)
|
|
708
|
+
|
|
709
|
+
def _registerWithoutUri(self, idem: Optional[IdemKey] = None) -> RegistrationFile:
|
|
710
|
+
"""Register without URI (IPFS flow step 1) with metadata."""
|
|
711
|
+
# Collect metadata for registration
|
|
712
|
+
metadata_entries = self._collectMetadataForRegistration()
|
|
713
|
+
|
|
714
|
+
# Mint agent with metadata
|
|
715
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
716
|
+
self.sdk.identity_registry,
|
|
717
|
+
"register",
|
|
718
|
+
"", # Empty tokenUri for now
|
|
719
|
+
metadata_entries
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# Wait for transaction
|
|
723
|
+
receipt = self.sdk.web3_client.wait_for_transaction(txHash)
|
|
724
|
+
|
|
725
|
+
# Get agent ID from events
|
|
726
|
+
agentId = self._extractAgentIdFromReceipt(receipt)
|
|
727
|
+
|
|
728
|
+
# Update registration file
|
|
729
|
+
self.registration_file.agentId = f"{self.sdk.chain_id()}:{agentId}"
|
|
730
|
+
self.registration_file.updatedAt = int(time.time())
|
|
731
|
+
|
|
732
|
+
return self.registration_file
|
|
733
|
+
|
|
734
|
+
def _registerWithUri(self, agentURI: URI, idem: Optional[IdemKey] = None) -> RegistrationFile:
|
|
735
|
+
"""Register with direct URI and metadata."""
|
|
736
|
+
# Update registration file
|
|
737
|
+
self.registration_file.agentURI = agentURI
|
|
738
|
+
self.registration_file.updatedAt = int(time.time())
|
|
739
|
+
|
|
740
|
+
# Collect metadata for registration
|
|
741
|
+
metadata_entries = self._collectMetadataForRegistration()
|
|
742
|
+
|
|
743
|
+
# Mint agent with URI and metadata
|
|
744
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
745
|
+
self.sdk.identity_registry,
|
|
746
|
+
"register",
|
|
747
|
+
agentURI,
|
|
748
|
+
metadata_entries
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# Wait for transaction
|
|
752
|
+
receipt = self.sdk.web3_client.wait_for_transaction(txHash)
|
|
753
|
+
|
|
754
|
+
# Get agent ID from events
|
|
755
|
+
agentId = self._extractAgentIdFromReceipt(receipt)
|
|
756
|
+
|
|
757
|
+
# Update registration file
|
|
758
|
+
self.registration_file.agentId = f"{self.sdk.chain_id()}:{agentId}"
|
|
759
|
+
self.registration_file.updatedAt = int(time.time())
|
|
760
|
+
|
|
761
|
+
return self.registration_file
|
|
762
|
+
|
|
763
|
+
def _extractAgentIdFromReceipt(self, receipt: Dict[str, Any]) -> int:
|
|
764
|
+
"""Extract agent ID from transaction receipt."""
|
|
765
|
+
# Look for Transfer event (ERC-721)
|
|
766
|
+
for i, log in enumerate(receipt.get('logs', [])):
|
|
767
|
+
try:
|
|
768
|
+
topics = log.get('topics', [])
|
|
769
|
+
if len(topics) >= 4:
|
|
770
|
+
topic0 = topics[0].hex()
|
|
771
|
+
# Check if this is a Transfer event (ERC-721) by looking at the topic
|
|
772
|
+
if topic0 == 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef':
|
|
773
|
+
# The fourth topic should contain the token ID
|
|
774
|
+
agentId_hex = topics[3].hex()
|
|
775
|
+
agentId = int(agentId_hex, 16)
|
|
776
|
+
return agentId
|
|
777
|
+
except Exception:
|
|
778
|
+
continue
|
|
779
|
+
|
|
780
|
+
# If no Transfer event found, try to get the token ID from the transaction
|
|
781
|
+
# This is a fallback for cases where the event might not be properly indexed
|
|
782
|
+
try:
|
|
783
|
+
# Get the transaction details
|
|
784
|
+
tx = self.sdk.web3_client.w3.eth.get_transaction(receipt['transactionHash'])
|
|
785
|
+
|
|
786
|
+
# Try to call the contract to get the latest token ID
|
|
787
|
+
# This assumes the contract has a method to get the total supply or latest ID
|
|
788
|
+
try:
|
|
789
|
+
total_supply = self.sdk.identity_registry.functions.totalSupply().call()
|
|
790
|
+
if total_supply > 0:
|
|
791
|
+
# Return the latest token ID (total supply - 1, since it's 0-indexed)
|
|
792
|
+
agentId = total_supply - 1
|
|
793
|
+
return agentId
|
|
794
|
+
except Exception:
|
|
795
|
+
pass
|
|
796
|
+
|
|
797
|
+
except Exception:
|
|
798
|
+
pass
|
|
799
|
+
|
|
800
|
+
raise ValueError("Could not extract agent ID from transaction receipt")
|
|
801
|
+
|
|
802
|
+
def updateRegistration(
|
|
803
|
+
self,
|
|
804
|
+
agentURI: Optional[URI] = None,
|
|
805
|
+
idem: Optional[IdemKey] = None,
|
|
806
|
+
) -> RegistrationFile:
|
|
807
|
+
"""Update registration after edits."""
|
|
808
|
+
if not self.registration_file.agentId:
|
|
809
|
+
raise ValueError("Agent must be registered before updating")
|
|
810
|
+
|
|
811
|
+
# Update URI if provided
|
|
812
|
+
if agentURI is not None:
|
|
813
|
+
self.registration_file.agentURI = agentURI
|
|
814
|
+
|
|
815
|
+
# Update timestamp
|
|
816
|
+
self.registration_file.updatedAt = int(time.time())
|
|
817
|
+
|
|
818
|
+
# Update on-chain URI if needed
|
|
819
|
+
if agentURI is not None:
|
|
820
|
+
agentId = int(self.registration_file.agentId.split(":")[-1])
|
|
821
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
822
|
+
self.sdk.identity_registry,
|
|
823
|
+
"setAgentUri",
|
|
824
|
+
agentId,
|
|
825
|
+
agentURI
|
|
826
|
+
)
|
|
827
|
+
self.sdk.web3_client.wait_for_transaction(txHash)
|
|
828
|
+
|
|
829
|
+
return self.registration_file
|
|
830
|
+
|
|
831
|
+
def setAgentUri(self, uri: str) -> 'Agent':
|
|
832
|
+
"""Set the agent URI in registration file (will be saved on-chain during next register call)."""
|
|
833
|
+
if not self.registration_file.agentId:
|
|
834
|
+
raise ValueError("Agent must be registered before setting URI")
|
|
835
|
+
|
|
836
|
+
# Update local registration file
|
|
837
|
+
self.registration_file.agentURI = uri
|
|
838
|
+
self.registration_file.updatedAt = int(time.time())
|
|
839
|
+
|
|
840
|
+
return self
|
|
841
|
+
|
|
842
|
+
# Ownership and lifecycle controls
|
|
843
|
+
def transfer(
|
|
844
|
+
self,
|
|
845
|
+
to: Address,
|
|
846
|
+
approve_operator: bool = False,
|
|
847
|
+
idem: Optional[IdemKey] = None,
|
|
848
|
+
) -> Dict[str, Any]:
|
|
849
|
+
"""Transfer agent ownership."""
|
|
850
|
+
if not self.registration_file.agentId:
|
|
851
|
+
raise ValueError("Agent must be registered before transferring")
|
|
852
|
+
|
|
853
|
+
agentId = int(self.registration_file.agentId.split(":")[-1])
|
|
854
|
+
|
|
855
|
+
# Transfer ownership
|
|
856
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
857
|
+
self.sdk.identity_registry,
|
|
858
|
+
"transferFrom",
|
|
859
|
+
self.sdk.web3_client.account.address,
|
|
860
|
+
to,
|
|
861
|
+
agentId
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
receipt = self.sdk.web3_client.wait_for_transaction(txHash)
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
"txHash": txHash,
|
|
868
|
+
"agentId": self.registration_file.agentId,
|
|
869
|
+
"from": self.sdk.web3_client.account.address,
|
|
870
|
+
"to": to
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
def addOperator(self, operator: Address, idem: Optional[IdemKey] = None) -> Dict[str, Any]:
|
|
874
|
+
"""Add operator (setApprovalForAll)."""
|
|
875
|
+
if not self.registration_file.agentId:
|
|
876
|
+
raise ValueError("Agent must be registered before adding operators")
|
|
877
|
+
|
|
878
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
879
|
+
self.sdk.identity_registry,
|
|
880
|
+
"setApprovalForAll",
|
|
881
|
+
operator,
|
|
882
|
+
True
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
receipt = self.sdk.web3_client.wait_for_transaction(txHash)
|
|
886
|
+
|
|
887
|
+
return {"txHash": txHash, "operator": operator}
|
|
888
|
+
|
|
889
|
+
def removeOperator(self, operator: Address, idem: Optional[IdemKey] = None) -> Dict[str, Any]:
|
|
890
|
+
"""Remove operator."""
|
|
891
|
+
if not self.registration_file.agentId:
|
|
892
|
+
raise ValueError("Agent must be registered before removing operators")
|
|
893
|
+
|
|
894
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
895
|
+
self.sdk.identity_registry,
|
|
896
|
+
"setApprovalForAll",
|
|
897
|
+
operator,
|
|
898
|
+
False
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
receipt = self.sdk.web3_client.wait_for_transaction(txHash)
|
|
902
|
+
|
|
903
|
+
return {"txHash": txHash, "operator": operator}
|
|
904
|
+
|
|
905
|
+
def transfer(self, newOwnerAddress: str) -> Dict[str, Any]:
|
|
906
|
+
"""Transfer agent ownership to a new address.
|
|
907
|
+
|
|
908
|
+
Only the current owner can transfer the agent.
|
|
909
|
+
|
|
910
|
+
Args:
|
|
911
|
+
newOwnerAddress: Ethereum address of the new owner
|
|
912
|
+
|
|
913
|
+
Returns:
|
|
914
|
+
Transaction receipt
|
|
915
|
+
|
|
916
|
+
Raises:
|
|
917
|
+
ValueError: If address is invalid or transfer not allowed
|
|
918
|
+
"""
|
|
919
|
+
if not self.registration_file.agentId:
|
|
920
|
+
raise ValueError("Agent must be registered before transfer")
|
|
921
|
+
|
|
922
|
+
# Validate new owner address
|
|
923
|
+
if not newOwnerAddress or newOwnerAddress == "0x0000000000000000000000000000000000000000":
|
|
924
|
+
raise ValueError("New owner address cannot be zero address")
|
|
925
|
+
|
|
926
|
+
# Get current owner using SDK utility
|
|
927
|
+
currentOwner = self.sdk.getAgentOwner(self.registration_file.agentId)
|
|
928
|
+
|
|
929
|
+
# Check if caller is the current owner
|
|
930
|
+
callerAddress = self.sdk.web3_client.account.address
|
|
931
|
+
if callerAddress.lower() != currentOwner.lower():
|
|
932
|
+
raise ValueError(f"Only the current owner ({currentOwner}) can transfer the agent")
|
|
933
|
+
|
|
934
|
+
# Prevent self-transfer
|
|
935
|
+
if newOwnerAddress.lower() == currentOwner.lower():
|
|
936
|
+
raise ValueError("Cannot transfer to the same owner")
|
|
937
|
+
|
|
938
|
+
# Validate address format (basic checksum validation)
|
|
939
|
+
try:
|
|
940
|
+
# Convert to checksum format for validation
|
|
941
|
+
checksum_address = self.sdk.web3_client.w3.to_checksum_address(newOwnerAddress)
|
|
942
|
+
except Exception as e:
|
|
943
|
+
raise ValueError(f"Invalid address format: {e}")
|
|
944
|
+
|
|
945
|
+
logger.debug(f"Transferring agent {self.registration_file.agentId} from {currentOwner} to {checksum_address}")
|
|
946
|
+
|
|
947
|
+
# Parse agentId to extract tokenId for contract call
|
|
948
|
+
agent_id_str = str(self.registration_file.agentId)
|
|
949
|
+
if ":" in agent_id_str:
|
|
950
|
+
token_id = int(agent_id_str.split(":")[-1])
|
|
951
|
+
else:
|
|
952
|
+
token_id = int(agent_id_str)
|
|
953
|
+
|
|
954
|
+
# Call transferFrom on the IdentityRegistry contract
|
|
955
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
956
|
+
self.sdk.identity_registry,
|
|
957
|
+
"transferFrom",
|
|
958
|
+
currentOwner,
|
|
959
|
+
checksum_address,
|
|
960
|
+
token_id
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
receipt = self.sdk.web3_client.wait_for_transaction(txHash)
|
|
964
|
+
|
|
965
|
+
logger.debug(f"Agent {self.registration_file.agentId} successfully transferred to {checksum_address}")
|
|
966
|
+
|
|
967
|
+
return {"txHash": txHash, "from": currentOwner, "to": checksum_address, "agentId": self.registration_file.agentId}
|
|
968
|
+
|
|
969
|
+
def activate(self, idem: Optional[IdemKey] = None) -> RegistrationFile:
|
|
970
|
+
"""Activate agent (soft "undelete")."""
|
|
971
|
+
self.registration_file.active = True
|
|
972
|
+
self.registration_file.updatedAt = int(time.time())
|
|
973
|
+
return self.registration_file
|
|
974
|
+
|
|
975
|
+
def deactivate(self, idem: Optional[IdemKey] = None) -> RegistrationFile:
|
|
976
|
+
"""Deactivate agent (soft "delete")."""
|
|
977
|
+
self.registration_file.active = False
|
|
978
|
+
self.registration_file.updatedAt = int(time.time())
|
|
979
|
+
return self.registration_file
|
|
980
|
+
|
|
981
|
+
# Utility methods
|
|
982
|
+
def toJson(self) -> str:
|
|
983
|
+
"""Convert registration file to JSON."""
|
|
984
|
+
return json.dumps(self.registration_file.to_dict(
|
|
985
|
+
chain_id=self.sdk.chain_id(),
|
|
986
|
+
identity_registry_address=self.sdk.identity_registry.address if self.sdk.identity_registry else None
|
|
987
|
+
), indent=2)
|
|
988
|
+
|
|
989
|
+
def saveToFile(self, filePath: str) -> None:
|
|
990
|
+
"""Save registration file to local file."""
|
|
991
|
+
with open(filePath, 'w') as f:
|
|
992
|
+
f.write(self.to_json())
|