agent0-sdk 0.3rc1__py3-none-any.whl → 0.5__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 +1 -1
- agent0_sdk/core/agent.py +303 -29
- agent0_sdk/core/contracts.py +93 -58
- agent0_sdk/core/feedback_manager.py +90 -161
- agent0_sdk/core/indexer.py +54 -26
- agent0_sdk/core/models.py +7 -19
- agent0_sdk/core/oasf_validator.py +98 -0
- agent0_sdk/core/sdk.py +31 -16
- agent0_sdk/core/subgraph_client.py +34 -15
- agent0_sdk/core/web3_client.py +184 -17
- agent0_sdk/taxonomies/all_domains.json +1565 -0
- agent0_sdk/taxonomies/all_skills.json +1030 -0
- {agent0_sdk-0.3rc1.dist-info → agent0_sdk-0.5.dist-info}/METADATA +78 -5
- agent0_sdk-0.5.dist-info/RECORD +19 -0
- {agent0_sdk-0.3rc1.dist-info → agent0_sdk-0.5.dist-info}/top_level.txt +0 -1
- agent0_sdk-0.3rc1.dist-info/RECORD +0 -29
- tests/__init__.py +0 -1
- tests/config.py +0 -46
- tests/conftest.py +0 -22
- tests/discover_test_data.py +0 -445
- tests/test_feedback.py +0 -417
- tests/test_models.py +0 -224
- tests/test_multi_chain.py +0 -588
- tests/test_real_public_servers.py +0 -103
- tests/test_registration.py +0 -267
- tests/test_registrationIpfs.py +0 -227
- tests/test_sdk.py +0 -240
- tests/test_search.py +0 -415
- tests/test_transfer.py +0 -255
- {agent0_sdk-0.3rc1.dist-info → agent0_sdk-0.5.dist-info}/WHEEL +0 -0
- {agent0_sdk-0.3rc1.dist-info → agent0_sdk-0.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OASF taxonomy validation utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# Cache for loaded taxonomy data
|
|
11
|
+
_skills_cache: Optional[dict] = None
|
|
12
|
+
_domains_cache: Optional[dict] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_taxonomy_path(filename: str) -> Path:
|
|
16
|
+
"""Get the path to a taxonomy file."""
|
|
17
|
+
# Get the directory where this file is located
|
|
18
|
+
current_dir = Path(__file__).parent
|
|
19
|
+
# Go up one level to agent0_sdk, then into taxonomies
|
|
20
|
+
taxonomy_dir = current_dir.parent / "taxonomies"
|
|
21
|
+
return taxonomy_dir / filename
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_skills() -> dict:
|
|
25
|
+
"""Load skills taxonomy file with caching."""
|
|
26
|
+
global _skills_cache
|
|
27
|
+
if _skills_cache is None:
|
|
28
|
+
skills_path = _get_taxonomy_path("all_skills.json")
|
|
29
|
+
try:
|
|
30
|
+
with open(skills_path, "r", encoding="utf-8") as f:
|
|
31
|
+
_skills_cache = json.load(f)
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
raise FileNotFoundError(
|
|
34
|
+
f"Skills taxonomy file not found: {skills_path}"
|
|
35
|
+
)
|
|
36
|
+
except json.JSONDecodeError as e:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Invalid JSON in skills taxonomy file: {e}"
|
|
39
|
+
)
|
|
40
|
+
return _skills_cache
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_domains() -> dict:
|
|
44
|
+
"""Load domains taxonomy file with caching."""
|
|
45
|
+
global _domains_cache
|
|
46
|
+
if _domains_cache is None:
|
|
47
|
+
domains_path = _get_taxonomy_path("all_domains.json")
|
|
48
|
+
try:
|
|
49
|
+
with open(domains_path, "r", encoding="utf-8") as f:
|
|
50
|
+
_domains_cache = json.load(f)
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
raise FileNotFoundError(
|
|
53
|
+
f"Domains taxonomy file not found: {domains_path}"
|
|
54
|
+
)
|
|
55
|
+
except json.JSONDecodeError as e:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Invalid JSON in domains taxonomy file: {e}"
|
|
58
|
+
)
|
|
59
|
+
return _domains_cache
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_skill(slug: str) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Validate if a skill slug exists in the OASF taxonomy.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
slug: The skill slug to validate (e.g., "natural_language_processing/natural_language_generation/summarization")
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if the skill exists in the taxonomy, False otherwise
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
FileNotFoundError: If the taxonomy file cannot be found
|
|
74
|
+
ValueError: If the taxonomy file is invalid JSON
|
|
75
|
+
"""
|
|
76
|
+
skills_data = _load_skills()
|
|
77
|
+
skills = skills_data.get("skills", {})
|
|
78
|
+
return slug in skills
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_domain(slug: str) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Validate if a domain slug exists in the OASF taxonomy.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
slug: The domain slug to validate (e.g., "finance_and_business/investment_services")
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if the domain exists in the taxonomy, False otherwise
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
FileNotFoundError: If the taxonomy file cannot be found
|
|
93
|
+
ValueError: If the taxonomy file is invalid JSON
|
|
94
|
+
"""
|
|
95
|
+
domains_data = _load_domains()
|
|
96
|
+
domains = domains_data.get("domains", {})
|
|
97
|
+
return slug in domains
|
|
98
|
+
|
agent0_sdk/core/sdk.py
CHANGED
|
@@ -278,7 +278,12 @@ class SDK:
|
|
|
278
278
|
return Agent(sdk=self, registration_file=registration_file)
|
|
279
279
|
|
|
280
280
|
def loadAgent(self, agentId: AgentId) -> Agent:
|
|
281
|
-
"""Load an existing agent (hydrates from registration file if registered).
|
|
281
|
+
"""Load an existing agent (hydrates from registration file if registered).
|
|
282
|
+
|
|
283
|
+
Note: Agents can be minted with an empty token URI (e.g. IPFS flow where publish fails).
|
|
284
|
+
In that case we return a partially-hydrated Agent with an empty registration file so the
|
|
285
|
+
caller can resume publishing and set the URI later.
|
|
286
|
+
"""
|
|
282
287
|
# Convert agentId to string if it's an integer
|
|
283
288
|
agentId = str(agentId)
|
|
284
289
|
|
|
@@ -292,16 +297,22 @@ class SDK:
|
|
|
292
297
|
|
|
293
298
|
# Get token URI from contract
|
|
294
299
|
try:
|
|
295
|
-
|
|
296
|
-
self.identity_registry, "tokenURI", int(token_id)
|
|
300
|
+
agent_uri = self.web3_client.call_contract(
|
|
301
|
+
self.identity_registry, "tokenURI", int(token_id) # tokenURI is ERC-721 standard, but represents agentURI
|
|
297
302
|
)
|
|
298
303
|
except Exception as e:
|
|
299
304
|
raise ValueError(f"Failed to load agent {agentId}: {e}")
|
|
300
305
|
|
|
301
|
-
# Load registration file
|
|
302
|
-
registration_file = self._load_registration_file(
|
|
306
|
+
# Load registration file (or fall back to a minimal file if agent URI is missing)
|
|
307
|
+
registration_file = self._load_registration_file(agent_uri)
|
|
303
308
|
registration_file.agentId = agentId
|
|
304
|
-
registration_file.agentURI =
|
|
309
|
+
registration_file.agentURI = agent_uri if agent_uri else None
|
|
310
|
+
|
|
311
|
+
if not agent_uri or not str(agent_uri).strip():
|
|
312
|
+
logger.warning(
|
|
313
|
+
f"Agent {agentId} has no agentURI set on-chain yet. "
|
|
314
|
+
"Returning a partial agent; update info and call registerIPFS() to publish and set URI."
|
|
315
|
+
)
|
|
305
316
|
|
|
306
317
|
# Store registry address for proper JSON generation
|
|
307
318
|
registry_address = self._registries.get("IDENTITY")
|
|
@@ -315,7 +326,13 @@ class SDK:
|
|
|
315
326
|
return Agent(sdk=self, registration_file=registration_file)
|
|
316
327
|
|
|
317
328
|
def _load_registration_file(self, uri: str) -> RegistrationFile:
|
|
318
|
-
"""Load registration file from URI.
|
|
329
|
+
"""Load registration file from URI.
|
|
330
|
+
|
|
331
|
+
If uri is empty/None/whitespace, returns an empty RegistrationFile to allow resume flows.
|
|
332
|
+
"""
|
|
333
|
+
if not uri or not str(uri).strip():
|
|
334
|
+
return RegistrationFile()
|
|
335
|
+
|
|
319
336
|
if uri.startswith("ipfs://"):
|
|
320
337
|
if not self.ipfs_client:
|
|
321
338
|
raise ValueError("IPFS client not configured")
|
|
@@ -346,21 +363,20 @@ class SDK:
|
|
|
346
363
|
# For now, we'll leave it empty
|
|
347
364
|
registration_file.operators = []
|
|
348
365
|
|
|
349
|
-
# Hydrate
|
|
366
|
+
# Hydrate agentWallet from on-chain (now uses getAgentWallet() instead of metadata)
|
|
350
367
|
agent_id = token_id
|
|
351
368
|
try:
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
self.identity_registry, "
|
|
369
|
+
# Get agentWallet using the new dedicated function
|
|
370
|
+
wallet_address = self.web3_client.call_contract(
|
|
371
|
+
self.identity_registry, "getAgentWallet", agent_id
|
|
355
372
|
)
|
|
356
|
-
if
|
|
357
|
-
wallet_address = "0x" + wallet_bytes.hex()
|
|
373
|
+
if wallet_address and wallet_address != "0x0000000000000000000000000000000000000000":
|
|
358
374
|
registration_file.walletAddress = wallet_address
|
|
359
375
|
# If wallet is read from on-chain, use current chain ID
|
|
360
376
|
# (the chain ID from the registration file might be outdated)
|
|
361
377
|
registration_file.walletChainId = self.chainId
|
|
362
378
|
except Exception as e:
|
|
363
|
-
# No on-chain wallet, will fall back to registration file
|
|
379
|
+
# No on-chain wallet set, will fall back to registration file
|
|
364
380
|
pass
|
|
365
381
|
|
|
366
382
|
try:
|
|
@@ -904,11 +920,10 @@ class SDK:
|
|
|
904
920
|
agentId: "AgentId",
|
|
905
921
|
feedbackFile: Dict[str, Any],
|
|
906
922
|
idem: Optional["IdemKey"] = None,
|
|
907
|
-
feedbackAuth: Optional[bytes] = None,
|
|
908
923
|
) -> "Feedback":
|
|
909
924
|
"""Give feedback (maps 8004 endpoint)."""
|
|
910
925
|
return self.feedback_manager.giveFeedback(
|
|
911
|
-
agentId, feedbackFile, idem
|
|
926
|
+
agentId, feedbackFile, idem
|
|
912
927
|
)
|
|
913
928
|
|
|
914
929
|
def getFeedback(
|
|
@@ -30,25 +30,38 @@ class SubgraphClient:
|
|
|
30
30
|
Returns:
|
|
31
31
|
JSON response from the subgraph
|
|
32
32
|
"""
|
|
33
|
-
|
|
33
|
+
def _do_query(q: str) -> Dict[str, Any]:
|
|
34
34
|
response = requests.post(
|
|
35
35
|
self.subgraph_url,
|
|
36
|
-
json={
|
|
37
|
-
'query': query,
|
|
38
|
-
'variables': variables or {}
|
|
39
|
-
},
|
|
36
|
+
json={'query': q, 'variables': variables or {}},
|
|
40
37
|
headers={'Content-Type': 'application/json'},
|
|
41
|
-
timeout=10
|
|
38
|
+
timeout=10,
|
|
42
39
|
)
|
|
43
40
|
response.raise_for_status()
|
|
44
41
|
result = response.json()
|
|
45
|
-
|
|
46
|
-
# Check for GraphQL errors
|
|
47
42
|
if 'errors' in result:
|
|
48
43
|
error_messages = [err.get('message', 'Unknown error') for err in result['errors']]
|
|
49
44
|
raise ValueError(f"GraphQL errors: {', '.join(error_messages)}")
|
|
50
|
-
|
|
51
45
|
return result.get('data', {})
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
return _do_query(query)
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
# Backwards/forwards compatibility for hosted subgraphs:
|
|
51
|
+
# Some deployments still expose `responseUri` instead of `responseURI`.
|
|
52
|
+
msg = str(e)
|
|
53
|
+
if ("has no field" in msg and "responseURI" in msg) and ("responseURI" in query):
|
|
54
|
+
logger.debug("Subgraph schema missing responseURI; retrying query with responseUri")
|
|
55
|
+
return _do_query(query.replace("responseURI", "responseUri"))
|
|
56
|
+
# Some deployments don't expose agentWallet fields on AgentRegistrationFile.
|
|
57
|
+
if (
|
|
58
|
+
"Type `AgentRegistrationFile` has no field `agentWallet`" in msg
|
|
59
|
+
or "Type `AgentRegistrationFile` has no field `agentWalletChainId`" in msg
|
|
60
|
+
):
|
|
61
|
+
logger.debug("Subgraph schema missing agentWallet fields; retrying query without them")
|
|
62
|
+
q2 = query.replace("agentWalletChainId", "").replace("agentWallet", "")
|
|
63
|
+
return _do_query(q2)
|
|
64
|
+
raise
|
|
52
65
|
except requests.exceptions.RequestException as e:
|
|
53
66
|
raise ConnectionError(f"Failed to query subgraph: {e}")
|
|
54
67
|
|
|
@@ -248,10 +261,12 @@ class SubgraphClient:
|
|
|
248
261
|
) {{
|
|
249
262
|
id
|
|
250
263
|
score
|
|
264
|
+
feedbackIndex
|
|
251
265
|
tag1
|
|
252
266
|
tag2
|
|
267
|
+
endpoint
|
|
253
268
|
clientAddress
|
|
254
|
-
|
|
269
|
+
feedbackURI
|
|
255
270
|
feedbackURIType
|
|
256
271
|
feedbackHash
|
|
257
272
|
isRevoked
|
|
@@ -276,7 +291,7 @@ class SubgraphClient:
|
|
|
276
291
|
responses {{
|
|
277
292
|
id
|
|
278
293
|
responder
|
|
279
|
-
|
|
294
|
+
responseURI
|
|
280
295
|
responseHash
|
|
281
296
|
createdAt
|
|
282
297
|
}}
|
|
@@ -400,10 +415,12 @@ class SubgraphClient:
|
|
|
400
415
|
id
|
|
401
416
|
agent { id agentId chainId }
|
|
402
417
|
clientAddress
|
|
418
|
+
feedbackIndex
|
|
403
419
|
score
|
|
404
420
|
tag1
|
|
405
421
|
tag2
|
|
406
|
-
|
|
422
|
+
endpoint
|
|
423
|
+
feedbackURI
|
|
407
424
|
feedbackURIType
|
|
408
425
|
feedbackHash
|
|
409
426
|
isRevoked
|
|
@@ -429,7 +446,7 @@ class SubgraphClient:
|
|
|
429
446
|
responses {
|
|
430
447
|
id
|
|
431
448
|
responder
|
|
432
|
-
|
|
449
|
+
responseURI
|
|
433
450
|
responseHash
|
|
434
451
|
createdAt
|
|
435
452
|
}
|
|
@@ -548,10 +565,12 @@ class SubgraphClient:
|
|
|
548
565
|
id
|
|
549
566
|
agent {{ id agentId chainId }}
|
|
550
567
|
clientAddress
|
|
568
|
+
feedbackIndex
|
|
551
569
|
score
|
|
552
570
|
tag1
|
|
553
571
|
tag2
|
|
554
|
-
|
|
572
|
+
endpoint
|
|
573
|
+
feedbackURI
|
|
555
574
|
feedbackURIType
|
|
556
575
|
feedbackHash
|
|
557
576
|
isRevoked
|
|
@@ -577,7 +596,7 @@ class SubgraphClient:
|
|
|
577
596
|
responses {{
|
|
578
597
|
id
|
|
579
598
|
responder
|
|
580
|
-
|
|
599
|
+
responseURI
|
|
581
600
|
responseHash
|
|
582
601
|
createdAt
|
|
583
602
|
}}
|
agent0_sdk/core/web3_client.py
CHANGED
|
@@ -5,7 +5,7 @@ Web3 integration layer for smart contract interactions.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple, Union, Callable
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from web3 import Web3
|
|
@@ -123,22 +123,6 @@ class Web3Client:
|
|
|
123
123
|
|
|
124
124
|
return event_filter.get_all_entries()
|
|
125
125
|
|
|
126
|
-
def encodeFeedbackAuth(
|
|
127
|
-
self,
|
|
128
|
-
agentId: int,
|
|
129
|
-
clientAddress: str,
|
|
130
|
-
indexLimit: int,
|
|
131
|
-
expiry: int,
|
|
132
|
-
chainId: int,
|
|
133
|
-
identityRegistry: str,
|
|
134
|
-
signerAddress: str
|
|
135
|
-
) -> bytes:
|
|
136
|
-
"""Encode feedback authorization data."""
|
|
137
|
-
return self.w3.codec.encode(
|
|
138
|
-
['uint256', 'address', 'uint64', 'uint256', 'uint256', 'address', 'address'],
|
|
139
|
-
[agentId, clientAddress, indexLimit, expiry, chainId, identityRegistry, signerAddress]
|
|
140
|
-
)
|
|
141
|
-
|
|
142
126
|
def signMessage(self, message: bytes) -> bytes:
|
|
143
127
|
"""Sign a message with the account's private key."""
|
|
144
128
|
# Create a SignableMessage from the raw bytes
|
|
@@ -190,3 +174,186 @@ class Web3Client:
|
|
|
190
174
|
def get_transaction_count(self, address: str) -> int:
|
|
191
175
|
"""Get transaction count (nonce) of an address."""
|
|
192
176
|
return self.w3.eth.get_transaction_count(address)
|
|
177
|
+
|
|
178
|
+
def encodeEIP712Domain(
|
|
179
|
+
self,
|
|
180
|
+
name: str,
|
|
181
|
+
version: str,
|
|
182
|
+
chain_id: int,
|
|
183
|
+
verifying_contract: str
|
|
184
|
+
) -> Dict[str, Any]:
|
|
185
|
+
"""Encode EIP-712 domain separator.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
name: Contract name
|
|
189
|
+
version: Contract version
|
|
190
|
+
chain_id: Chain ID
|
|
191
|
+
verifying_contract: Contract address
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Domain separator dictionary
|
|
195
|
+
"""
|
|
196
|
+
return {
|
|
197
|
+
"name": name,
|
|
198
|
+
"version": version,
|
|
199
|
+
"chainId": chain_id,
|
|
200
|
+
"verifyingContract": verifying_contract
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
def build_agent_wallet_set_typed_data(
|
|
204
|
+
self,
|
|
205
|
+
agent_id: int,
|
|
206
|
+
new_wallet: str,
|
|
207
|
+
owner: str,
|
|
208
|
+
deadline: int,
|
|
209
|
+
verifying_contract: str,
|
|
210
|
+
chain_id: int,
|
|
211
|
+
) -> Dict[str, Any]:
|
|
212
|
+
"""Build EIP-712 typed data for ERC-8004 IdentityRegistry setAgentWallet.
|
|
213
|
+
|
|
214
|
+
Contract expects:
|
|
215
|
+
- domain: name="ERC8004IdentityRegistry", version="1"
|
|
216
|
+
- primaryType: "AgentWalletSet"
|
|
217
|
+
- message: { agentId, newWallet, owner, deadline }
|
|
218
|
+
"""
|
|
219
|
+
domain = self.encodeEIP712Domain(
|
|
220
|
+
name="ERC8004IdentityRegistry",
|
|
221
|
+
version="1",
|
|
222
|
+
chain_id=chain_id,
|
|
223
|
+
verifying_contract=verifying_contract,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
message_types = {
|
|
227
|
+
"AgentWalletSet": [
|
|
228
|
+
{"name": "agentId", "type": "uint256"},
|
|
229
|
+
{"name": "newWallet", "type": "address"},
|
|
230
|
+
{"name": "owner", "type": "address"},
|
|
231
|
+
{"name": "deadline", "type": "uint256"},
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
message = {
|
|
236
|
+
"agentId": agent_id,
|
|
237
|
+
"newWallet": new_wallet,
|
|
238
|
+
"owner": owner,
|
|
239
|
+
"deadline": deadline,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# eth_account.messages.encode_typed_data expects the "full_message" format
|
|
243
|
+
return {
|
|
244
|
+
"types": {
|
|
245
|
+
"EIP712Domain": [
|
|
246
|
+
{"name": "name", "type": "string"},
|
|
247
|
+
{"name": "version", "type": "string"},
|
|
248
|
+
{"name": "chainId", "type": "uint256"},
|
|
249
|
+
{"name": "verifyingContract", "type": "address"},
|
|
250
|
+
],
|
|
251
|
+
**message_types,
|
|
252
|
+
},
|
|
253
|
+
"domain": domain,
|
|
254
|
+
"primaryType": "AgentWalletSet",
|
|
255
|
+
"message": message,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
def sign_typed_data(
|
|
259
|
+
self,
|
|
260
|
+
full_message: Dict[str, Any],
|
|
261
|
+
signer: Union[str, BaseAccount],
|
|
262
|
+
) -> bytes:
|
|
263
|
+
"""Sign EIP-712 typed data with a provided signer (EOA).
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
full_message: Typed data dict compatible with encode_typed_data(full_message=...)
|
|
267
|
+
signer: Private key string or eth_account BaseAccount/LocalAccount
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Signature bytes
|
|
271
|
+
"""
|
|
272
|
+
from eth_account.messages import encode_typed_data
|
|
273
|
+
|
|
274
|
+
if isinstance(signer, str):
|
|
275
|
+
acct: BaseAccount = Account.from_key(signer)
|
|
276
|
+
else:
|
|
277
|
+
acct = signer
|
|
278
|
+
|
|
279
|
+
encoded = encode_typed_data(full_message=full_message)
|
|
280
|
+
signed = acct.sign_message(encoded)
|
|
281
|
+
return signed.signature
|
|
282
|
+
|
|
283
|
+
def signEIP712Message(
|
|
284
|
+
self,
|
|
285
|
+
domain: Dict[str, Any],
|
|
286
|
+
message_types: Dict[str, List[Dict[str, str]]],
|
|
287
|
+
message: Dict[str, Any]
|
|
288
|
+
) -> bytes:
|
|
289
|
+
"""Sign an EIP-712 typed message.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
domain: EIP-712 domain separator
|
|
293
|
+
message_types: Type definitions for the message
|
|
294
|
+
message: Message data to sign
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Signature bytes
|
|
298
|
+
"""
|
|
299
|
+
if not self.account:
|
|
300
|
+
raise ValueError("Cannot sign message: SDK is in read-only mode. Provide a signer to enable signing.")
|
|
301
|
+
|
|
302
|
+
from eth_account.messages import encode_typed_data
|
|
303
|
+
|
|
304
|
+
structured_data = {
|
|
305
|
+
"types": {
|
|
306
|
+
"EIP712Domain": [
|
|
307
|
+
{"name": "name", "type": "string"},
|
|
308
|
+
{"name": "version", "type": "string"},
|
|
309
|
+
{"name": "chainId", "type": "uint256"},
|
|
310
|
+
{"name": "verifyingContract", "type": "address"}
|
|
311
|
+
],
|
|
312
|
+
**message_types
|
|
313
|
+
},
|
|
314
|
+
"domain": domain,
|
|
315
|
+
"primaryType": list(message_types.keys())[0] if message_types else "Message",
|
|
316
|
+
"message": message
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
encoded = encode_typed_data(full_message=structured_data)
|
|
320
|
+
signed = self.account.sign_message(encoded)
|
|
321
|
+
return signed.signature
|
|
322
|
+
|
|
323
|
+
def verifyEIP712Signature(
|
|
324
|
+
self,
|
|
325
|
+
domain: Dict[str, Any],
|
|
326
|
+
message_types: Dict[str, List[Dict[str, str]]],
|
|
327
|
+
message: Dict[str, Any],
|
|
328
|
+
signature: bytes
|
|
329
|
+
) -> str:
|
|
330
|
+
"""Verify an EIP-712 signature and recover the signer address.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
domain: EIP-712 domain separator
|
|
334
|
+
message_types: Type definitions for the message
|
|
335
|
+
message: Message data that was signed
|
|
336
|
+
signature: Signature bytes to verify
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Recovered signer address
|
|
340
|
+
"""
|
|
341
|
+
from eth_account.messages import encode_typed_data
|
|
342
|
+
|
|
343
|
+
structured_data = {
|
|
344
|
+
"types": {
|
|
345
|
+
"EIP712Domain": [
|
|
346
|
+
{"name": "name", "type": "string"},
|
|
347
|
+
{"name": "version", "type": "string"},
|
|
348
|
+
{"name": "chainId", "type": "uint256"},
|
|
349
|
+
{"name": "verifyingContract", "type": "address"}
|
|
350
|
+
],
|
|
351
|
+
**message_types
|
|
352
|
+
},
|
|
353
|
+
"domain": domain,
|
|
354
|
+
"primaryType": list(message_types.keys())[0] if message_types else "Message",
|
|
355
|
+
"message": message
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
encoded = encode_typed_data(full_message=structured_data)
|
|
359
|
+
return self.w3.eth.account.recover_message(encoded, signature=signature)
|