agent0-sdk 1.4.0__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 +57 -0
- agent0_sdk/core/agent.py +1187 -0
- agent0_sdk/core/contracts.py +547 -0
- agent0_sdk/core/endpoint_crawler.py +330 -0
- agent0_sdk/core/feedback_manager.py +1052 -0
- agent0_sdk/core/indexer.py +1837 -0
- agent0_sdk/core/ipfs_client.py +357 -0
- agent0_sdk/core/models.py +303 -0
- agent0_sdk/core/oasf_validator.py +98 -0
- agent0_sdk/core/sdk.py +1005 -0
- agent0_sdk/core/subgraph_client.py +853 -0
- agent0_sdk/core/transaction_handle.py +71 -0
- agent0_sdk/core/value_encoding.py +91 -0
- agent0_sdk/core/web3_client.py +399 -0
- agent0_sdk/taxonomies/all_domains.json +1565 -0
- agent0_sdk/taxonomies/all_skills.json +1030 -0
- agent0_sdk-1.4.0.dist-info/METADATA +403 -0
- agent0_sdk-1.4.0.dist-info/RECORD +21 -0
- agent0_sdk-1.4.0.dist-info/WHEEL +5 -0
- agent0_sdk-1.4.0.dist-info/licenses/LICENSE +22 -0
- agent0_sdk-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, Dict, Generic, TypeVar, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .web3_client import Web3Client
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TransactionMined(Generic[T]):
|
|
14
|
+
receipt: Dict[str, Any]
|
|
15
|
+
result: T
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TransactionHandle(Generic[T]):
|
|
19
|
+
"""
|
|
20
|
+
Transaction lifecycle handle (submitted-by-default).
|
|
21
|
+
|
|
22
|
+
- `tx_hash` is available immediately after submission.
|
|
23
|
+
- `wait_mined` / `wait_confirmed` can be called to await a receipt (and optional confirmations)
|
|
24
|
+
and produce a domain result.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
web3_client: Web3Client,
|
|
31
|
+
tx_hash: str,
|
|
32
|
+
compute_result: Callable[[Dict[str, Any]], T],
|
|
33
|
+
):
|
|
34
|
+
self.web3_client = web3_client
|
|
35
|
+
self.tx_hash = tx_hash
|
|
36
|
+
self._compute_result = compute_result
|
|
37
|
+
self._memo: Dict[str, TransactionMined[T]] = {}
|
|
38
|
+
|
|
39
|
+
def wait_mined(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
timeout: int = 60,
|
|
43
|
+
confirmations: int = 1,
|
|
44
|
+
throw_on_revert: bool = True,
|
|
45
|
+
) -> TransactionMined[T]:
|
|
46
|
+
key = f"{timeout}:{confirmations}:{int(bool(throw_on_revert))}"
|
|
47
|
+
existing = self._memo.get(key)
|
|
48
|
+
if existing is not None:
|
|
49
|
+
return existing
|
|
50
|
+
|
|
51
|
+
receipt = self.web3_client.wait_for_transaction(
|
|
52
|
+
self.tx_hash,
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
confirmations=confirmations,
|
|
55
|
+
throw_on_revert=throw_on_revert,
|
|
56
|
+
)
|
|
57
|
+
result = self._compute_result(receipt)
|
|
58
|
+
mined = TransactionMined(receipt=receipt, result=result)
|
|
59
|
+
self._memo[key] = mined
|
|
60
|
+
return mined
|
|
61
|
+
|
|
62
|
+
def wait_confirmed(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
timeout: int = 60,
|
|
66
|
+
confirmations: int = 1,
|
|
67
|
+
throw_on_revert: bool = True,
|
|
68
|
+
) -> TransactionMined[T]:
|
|
69
|
+
return self.wait_mined(timeout=timeout, confirmations=confirmations, throw_on_revert=throw_on_revert)
|
|
70
|
+
|
|
71
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Value encoding utilities for ReputationRegistry (Jan 2026).
|
|
3
|
+
|
|
4
|
+
On-chain representation: (value:int128, valueDecimals:uint8)
|
|
5
|
+
Human representation: value / 10^valueDecimals
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from decimal import Decimal, ROUND_HALF_UP, getcontext
|
|
12
|
+
from typing import Tuple, Union
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Plenty of headroom for scaling and clamping checks
|
|
17
|
+
getcontext().prec = 120
|
|
18
|
+
|
|
19
|
+
MAX_DECIMALS = 18
|
|
20
|
+
# Solidity constant (raw int128 magnitude). Contract enforces abs(value) <= 1e38.
|
|
21
|
+
MAX_ABS_VALUE_RAW = 10**38
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def encode_feedback_value(input_value: Union[int, float, str, Decimal]) -> Tuple[int, int, str]:
|
|
25
|
+
"""
|
|
26
|
+
Encode a user-facing value into the on-chain (value, valueDecimals) pair.
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
- str: parsed using Decimal (no float casting). If >18 decimals, it is rounded half-up to 18 decimals.
|
|
30
|
+
- float: accepted and rounded half-up to 18 decimals (never rejected).
|
|
31
|
+
- int/Decimal: treated similarly; Decimal preserves precision.
|
|
32
|
+
|
|
33
|
+
Returns: (value_raw:int, value_decimals:int, normalized:str)
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(input_value, Decimal):
|
|
36
|
+
dec = input_value
|
|
37
|
+
normalized = format(dec, "f")
|
|
38
|
+
elif isinstance(input_value, int):
|
|
39
|
+
dec = Decimal(input_value)
|
|
40
|
+
normalized = str(input_value)
|
|
41
|
+
elif isinstance(input_value, float):
|
|
42
|
+
# Avoid binary float artifacts by going through Decimal(str(x)), then quantize to 18 places.
|
|
43
|
+
dec = Decimal(str(input_value)).quantize(Decimal("1e-18"), rounding=ROUND_HALF_UP)
|
|
44
|
+
normalized = format(dec, "f")
|
|
45
|
+
elif isinstance(input_value, str):
|
|
46
|
+
s = input_value.strip()
|
|
47
|
+
if s == "":
|
|
48
|
+
raise ValueError("value cannot be an empty string")
|
|
49
|
+
dec = Decimal(s)
|
|
50
|
+
# Expand to plain decimal string (no exponent) for determining decimals
|
|
51
|
+
normalized = format(dec, "f")
|
|
52
|
+
else:
|
|
53
|
+
raise TypeError(f"value must be int|float|str|Decimal, got {type(input_value)}")
|
|
54
|
+
|
|
55
|
+
# Determine decimals from the normalized representation.
|
|
56
|
+
# This preserves trailing zeros for string inputs like "1.2300".
|
|
57
|
+
if "." in normalized:
|
|
58
|
+
decimals = len(normalized.split(".", 1)[1])
|
|
59
|
+
else:
|
|
60
|
+
decimals = 0
|
|
61
|
+
|
|
62
|
+
if decimals > MAX_DECIMALS:
|
|
63
|
+
dec = dec.quantize(Decimal("1e-18"), rounding=ROUND_HALF_UP)
|
|
64
|
+
normalized = format(dec, "f") # keeps fixed 18 decimals
|
|
65
|
+
decimals = MAX_DECIMALS
|
|
66
|
+
|
|
67
|
+
scale = Decimal(10) ** decimals
|
|
68
|
+
raw_decimal = dec * scale
|
|
69
|
+
raw_int = int(raw_decimal.to_integral_value(rounding=ROUND_HALF_UP))
|
|
70
|
+
|
|
71
|
+
if abs(raw_int) > MAX_ABS_VALUE_RAW:
|
|
72
|
+
raw_int = MAX_ABS_VALUE_RAW if raw_int > 0 else -MAX_ABS_VALUE_RAW
|
|
73
|
+
clamped = Decimal(raw_int) / (Decimal(10) ** decimals)
|
|
74
|
+
normalized = format(clamped, "f")
|
|
75
|
+
logger.warning(
|
|
76
|
+
"Feedback value %r exceeds on-chain max magnitude; clamped to %s (decimals=%s)",
|
|
77
|
+
input_value,
|
|
78
|
+
normalized,
|
|
79
|
+
decimals,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return raw_int, decimals, normalized
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def decode_feedback_value(value_raw: int, value_decimals: int) -> float:
|
|
86
|
+
"""Decode (value, valueDecimals) into a Python float."""
|
|
87
|
+
if value_decimals < 0:
|
|
88
|
+
raise ValueError("valueDecimals cannot be negative")
|
|
89
|
+
return float(Decimal(value_raw) / (Decimal(10) ** int(value_decimals)))
|
|
90
|
+
|
|
91
|
+
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Web3 integration layer for smart contract interactions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple, Union, Callable
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from web3 import Web3
|
|
13
|
+
from web3.contract import Contract
|
|
14
|
+
from eth_account import Account
|
|
15
|
+
from eth_account.signers.base import BaseAccount
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"Web3 dependencies not installed. Install with: pip install web3 eth-account"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Web3Client:
|
|
23
|
+
"""Web3 client for interacting with ERC-8004 smart contracts."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
rpc_url: str,
|
|
28
|
+
private_key: Optional[str] = None,
|
|
29
|
+
account: Optional[BaseAccount] = None,
|
|
30
|
+
):
|
|
31
|
+
"""Initialize Web3 client."""
|
|
32
|
+
self.rpc_url = rpc_url
|
|
33
|
+
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
34
|
+
if not self.w3.is_connected():
|
|
35
|
+
raise ConnectionError("Failed to connect to Ethereum node")
|
|
36
|
+
|
|
37
|
+
if account:
|
|
38
|
+
self.account = account
|
|
39
|
+
elif private_key:
|
|
40
|
+
self.account = Account.from_key(private_key)
|
|
41
|
+
else:
|
|
42
|
+
# Read-only mode - no account
|
|
43
|
+
self.account = None
|
|
44
|
+
|
|
45
|
+
self.chain_id = self.w3.eth.chain_id
|
|
46
|
+
|
|
47
|
+
def get_contract(self, address: str, abi: List[Dict[str, Any]]) -> Contract:
|
|
48
|
+
"""Get contract instance."""
|
|
49
|
+
return self.w3.eth.contract(address=address, abi=abi)
|
|
50
|
+
|
|
51
|
+
def call_contract(
|
|
52
|
+
self,
|
|
53
|
+
contract: Contract,
|
|
54
|
+
method_name: str,
|
|
55
|
+
*args,
|
|
56
|
+
**kwargs
|
|
57
|
+
) -> Any:
|
|
58
|
+
"""Call a contract method (view/pure)."""
|
|
59
|
+
method = getattr(contract.functions, method_name)
|
|
60
|
+
return method(*args, **kwargs).call()
|
|
61
|
+
|
|
62
|
+
def transact_contract(
|
|
63
|
+
self,
|
|
64
|
+
contract: Contract,
|
|
65
|
+
method_name: str,
|
|
66
|
+
*args,
|
|
67
|
+
gas_limit: Optional[int] = None,
|
|
68
|
+
gas_price: Optional[int] = None,
|
|
69
|
+
max_fee_per_gas: Optional[int] = None,
|
|
70
|
+
max_priority_fee_per_gas: Optional[int] = None,
|
|
71
|
+
**kwargs
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Execute a contract transaction."""
|
|
74
|
+
if not self.account:
|
|
75
|
+
raise ValueError("Cannot execute transaction: SDK is in read-only mode. Provide a signer to enable write operations.")
|
|
76
|
+
|
|
77
|
+
method = getattr(contract.functions, method_name)
|
|
78
|
+
|
|
79
|
+
# Build transaction with proper nonce management
|
|
80
|
+
# Use 'pending' to get the next nonce including pending transactions
|
|
81
|
+
nonce = self.w3.eth.get_transaction_count(self.account.address, 'pending')
|
|
82
|
+
tx = method(*args, **kwargs).build_transaction({
|
|
83
|
+
'from': self.account.address,
|
|
84
|
+
'nonce': nonce,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# Add gas settings
|
|
88
|
+
if gas_limit:
|
|
89
|
+
tx['gas'] = gas_limit
|
|
90
|
+
if gas_price:
|
|
91
|
+
tx['gasPrice'] = gas_price
|
|
92
|
+
if max_fee_per_gas:
|
|
93
|
+
tx['maxFeePerGas'] = max_fee_per_gas
|
|
94
|
+
if max_priority_fee_per_gas:
|
|
95
|
+
tx['maxPriorityFeePerGas'] = max_priority_fee_per_gas
|
|
96
|
+
|
|
97
|
+
# Sign and send
|
|
98
|
+
signed_tx = self.w3.eth.account.sign_transaction(tx, self.account.key)
|
|
99
|
+
tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction if hasattr(signed_tx, 'rawTransaction') else signed_tx.raw_transaction)
|
|
100
|
+
|
|
101
|
+
return tx_hash.hex()
|
|
102
|
+
|
|
103
|
+
def wait_for_transaction(
|
|
104
|
+
self,
|
|
105
|
+
tx_hash: str,
|
|
106
|
+
timeout: int = 60,
|
|
107
|
+
confirmations: int = 1,
|
|
108
|
+
throw_on_revert: bool = True,
|
|
109
|
+
) -> Dict[str, Any]:
|
|
110
|
+
"""Wait for transaction to be mined, optionally waiting for additional confirmations."""
|
|
111
|
+
if confirmations < 1:
|
|
112
|
+
raise ValueError("confirmations must be >= 1")
|
|
113
|
+
|
|
114
|
+
start = time.time()
|
|
115
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
|
|
116
|
+
|
|
117
|
+
if throw_on_revert:
|
|
118
|
+
status = receipt.get("status")
|
|
119
|
+
# Most chains return 1 for success, 0 for revert (may be int or HexBytes-like).
|
|
120
|
+
try:
|
|
121
|
+
status_int = int(status)
|
|
122
|
+
except Exception:
|
|
123
|
+
try:
|
|
124
|
+
status_int = int(status.hex(), 16) # type: ignore[attr-defined]
|
|
125
|
+
except Exception:
|
|
126
|
+
status_int = 1 # if unknown, don't falsely throw
|
|
127
|
+
if status_int == 0:
|
|
128
|
+
raise ValueError(f"Transaction reverted: {tx_hash}")
|
|
129
|
+
|
|
130
|
+
if confirmations > 1:
|
|
131
|
+
block_number = receipt.get("blockNumber")
|
|
132
|
+
if block_number is not None:
|
|
133
|
+
target_block = int(block_number) + (confirmations - 1)
|
|
134
|
+
while True:
|
|
135
|
+
current = int(self.w3.eth.block_number)
|
|
136
|
+
if current >= target_block:
|
|
137
|
+
break
|
|
138
|
+
if time.time() - start > timeout:
|
|
139
|
+
raise TimeoutError(
|
|
140
|
+
f"Timed out waiting for confirmations (tx={tx_hash}, confirmations={confirmations})"
|
|
141
|
+
)
|
|
142
|
+
time.sleep(1.0)
|
|
143
|
+
|
|
144
|
+
return receipt
|
|
145
|
+
|
|
146
|
+
def get_events(
|
|
147
|
+
self,
|
|
148
|
+
contract: Contract,
|
|
149
|
+
event_name: str,
|
|
150
|
+
from_block: int = 0,
|
|
151
|
+
to_block: Optional[int] = None,
|
|
152
|
+
argument_filters: Optional[Dict[str, Any]] = None
|
|
153
|
+
) -> List[Dict[str, Any]]:
|
|
154
|
+
"""Get contract events."""
|
|
155
|
+
if to_block is None:
|
|
156
|
+
to_block = self.w3.eth.block_number
|
|
157
|
+
|
|
158
|
+
event_filter = contract.events[event_name].create_filter(
|
|
159
|
+
fromBlock=from_block,
|
|
160
|
+
toBlock=to_block,
|
|
161
|
+
argument_filters=argument_filters or {}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return event_filter.get_all_entries()
|
|
165
|
+
|
|
166
|
+
def signMessage(self, message: bytes) -> bytes:
|
|
167
|
+
"""Sign a message with the account's private key."""
|
|
168
|
+
# Create a SignableMessage from the raw bytes
|
|
169
|
+
from eth_account.messages import encode_defunct
|
|
170
|
+
signableMessage = encode_defunct(message)
|
|
171
|
+
signedMessage = self.account.sign_message(signableMessage)
|
|
172
|
+
return signedMessage.signature
|
|
173
|
+
|
|
174
|
+
def recoverAddress(self, message: bytes, signature: bytes) -> str:
|
|
175
|
+
"""Recover address from message and signature."""
|
|
176
|
+
from eth_account.messages import encode_defunct
|
|
177
|
+
signable_message = encode_defunct(message)
|
|
178
|
+
return self.w3.eth.account.recover_message(signable_message, signature=signature)
|
|
179
|
+
|
|
180
|
+
def keccak256(self, data: bytes) -> bytes:
|
|
181
|
+
"""Compute Keccak-256 hash."""
|
|
182
|
+
return self.w3.keccak(data)
|
|
183
|
+
|
|
184
|
+
def to_checksum_address(self, address: str) -> str:
|
|
185
|
+
"""Convert address to checksum format."""
|
|
186
|
+
return self.w3.to_checksum_address(address)
|
|
187
|
+
|
|
188
|
+
def normalize_address(self, address: str) -> str:
|
|
189
|
+
"""Normalize address to lowercase for consistent storage and comparison.
|
|
190
|
+
|
|
191
|
+
Ethereum addresses are case-insensitive but EIP-55 checksum addresses
|
|
192
|
+
use mixed case. For storage and comparison purposes, we normalize to
|
|
193
|
+
lowercase to avoid case-sensitivity issues.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
address: Ethereum address (with or without checksum)
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Address in lowercase format
|
|
200
|
+
"""
|
|
201
|
+
# Remove 0x prefix if present, convert to lowercase, re-add prefix
|
|
202
|
+
if address.startswith("0x") or address.startswith("0X"):
|
|
203
|
+
return "0x" + address[2:].lower()
|
|
204
|
+
return address.lower()
|
|
205
|
+
|
|
206
|
+
def is_address(self, address: str) -> bool:
|
|
207
|
+
"""Check if string is a valid Ethereum address."""
|
|
208
|
+
return self.w3.is_address(address)
|
|
209
|
+
|
|
210
|
+
def get_balance(self, address: str) -> int:
|
|
211
|
+
"""Get ETH balance of an address."""
|
|
212
|
+
return self.w3.eth.get_balance(address)
|
|
213
|
+
|
|
214
|
+
def get_transaction_count(self, address: str) -> int:
|
|
215
|
+
"""Get transaction count (nonce) of an address."""
|
|
216
|
+
return self.w3.eth.get_transaction_count(address)
|
|
217
|
+
|
|
218
|
+
def encodeEIP712Domain(
|
|
219
|
+
self,
|
|
220
|
+
name: str,
|
|
221
|
+
version: str,
|
|
222
|
+
chain_id: int,
|
|
223
|
+
verifying_contract: str
|
|
224
|
+
) -> Dict[str, Any]:
|
|
225
|
+
"""Encode EIP-712 domain separator.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
name: Contract name
|
|
229
|
+
version: Contract version
|
|
230
|
+
chain_id: Chain ID
|
|
231
|
+
verifying_contract: Contract address
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Domain separator dictionary
|
|
235
|
+
"""
|
|
236
|
+
return {
|
|
237
|
+
"name": name,
|
|
238
|
+
"version": version,
|
|
239
|
+
"chainId": chain_id,
|
|
240
|
+
"verifyingContract": verifying_contract
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def build_agent_wallet_set_typed_data(
|
|
244
|
+
self,
|
|
245
|
+
agent_id: int,
|
|
246
|
+
new_wallet: str,
|
|
247
|
+
owner: str,
|
|
248
|
+
deadline: int,
|
|
249
|
+
verifying_contract: str,
|
|
250
|
+
chain_id: int,
|
|
251
|
+
) -> Dict[str, Any]:
|
|
252
|
+
"""Build EIP-712 typed data for the agent wallet verification message.
|
|
253
|
+
|
|
254
|
+
Contract expects:
|
|
255
|
+
- domain: name="ERC8004IdentityRegistry", version="1"
|
|
256
|
+
- primaryType: "AgentWalletSet"
|
|
257
|
+
- message: { agentId, newWallet, owner, deadline }
|
|
258
|
+
"""
|
|
259
|
+
domain = self.encodeEIP712Domain(
|
|
260
|
+
name="ERC8004IdentityRegistry",
|
|
261
|
+
version="1",
|
|
262
|
+
chain_id=chain_id,
|
|
263
|
+
verifying_contract=verifying_contract,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
message_types = {
|
|
267
|
+
"AgentWalletSet": [
|
|
268
|
+
{"name": "agentId", "type": "uint256"},
|
|
269
|
+
{"name": "newWallet", "type": "address"},
|
|
270
|
+
{"name": "owner", "type": "address"},
|
|
271
|
+
{"name": "deadline", "type": "uint256"},
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
message = {
|
|
276
|
+
"agentId": agent_id,
|
|
277
|
+
"newWallet": new_wallet,
|
|
278
|
+
"owner": owner,
|
|
279
|
+
"deadline": deadline,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# eth_account.messages.encode_typed_data expects the "full_message" format
|
|
283
|
+
return {
|
|
284
|
+
"types": {
|
|
285
|
+
"EIP712Domain": [
|
|
286
|
+
{"name": "name", "type": "string"},
|
|
287
|
+
{"name": "version", "type": "string"},
|
|
288
|
+
{"name": "chainId", "type": "uint256"},
|
|
289
|
+
{"name": "verifyingContract", "type": "address"},
|
|
290
|
+
],
|
|
291
|
+
**message_types,
|
|
292
|
+
},
|
|
293
|
+
"domain": domain,
|
|
294
|
+
"primaryType": "AgentWalletSet",
|
|
295
|
+
"message": message,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
def sign_typed_data(
|
|
299
|
+
self,
|
|
300
|
+
full_message: Dict[str, Any],
|
|
301
|
+
signer: Union[str, BaseAccount],
|
|
302
|
+
) -> bytes:
|
|
303
|
+
"""Sign EIP-712 typed data with a provided signer (EOA).
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
full_message: Typed data dict compatible with encode_typed_data(full_message=...)
|
|
307
|
+
signer: Private key string or eth_account BaseAccount/LocalAccount
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Signature bytes
|
|
311
|
+
"""
|
|
312
|
+
from eth_account.messages import encode_typed_data
|
|
313
|
+
|
|
314
|
+
if isinstance(signer, str):
|
|
315
|
+
acct: BaseAccount = Account.from_key(signer)
|
|
316
|
+
else:
|
|
317
|
+
acct = signer
|
|
318
|
+
|
|
319
|
+
encoded = encode_typed_data(full_message=full_message)
|
|
320
|
+
signed = acct.sign_message(encoded)
|
|
321
|
+
return signed.signature
|
|
322
|
+
|
|
323
|
+
def signEIP712Message(
|
|
324
|
+
self,
|
|
325
|
+
domain: Dict[str, Any],
|
|
326
|
+
message_types: Dict[str, List[Dict[str, str]]],
|
|
327
|
+
message: Dict[str, Any]
|
|
328
|
+
) -> bytes:
|
|
329
|
+
"""Sign an EIP-712 typed message.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
domain: EIP-712 domain separator
|
|
333
|
+
message_types: Type definitions for the message
|
|
334
|
+
message: Message data to sign
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Signature bytes
|
|
338
|
+
"""
|
|
339
|
+
if not self.account:
|
|
340
|
+
raise ValueError("Cannot sign message: SDK is in read-only mode. Provide a signer to enable signing.")
|
|
341
|
+
|
|
342
|
+
from eth_account.messages import encode_typed_data
|
|
343
|
+
|
|
344
|
+
structured_data = {
|
|
345
|
+
"types": {
|
|
346
|
+
"EIP712Domain": [
|
|
347
|
+
{"name": "name", "type": "string"},
|
|
348
|
+
{"name": "version", "type": "string"},
|
|
349
|
+
{"name": "chainId", "type": "uint256"},
|
|
350
|
+
{"name": "verifyingContract", "type": "address"}
|
|
351
|
+
],
|
|
352
|
+
**message_types
|
|
353
|
+
},
|
|
354
|
+
"domain": domain,
|
|
355
|
+
"primaryType": list(message_types.keys())[0] if message_types else "Message",
|
|
356
|
+
"message": message
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
encoded = encode_typed_data(full_message=structured_data)
|
|
360
|
+
signed = self.account.sign_message(encoded)
|
|
361
|
+
return signed.signature
|
|
362
|
+
|
|
363
|
+
def verifyEIP712Signature(
|
|
364
|
+
self,
|
|
365
|
+
domain: Dict[str, Any],
|
|
366
|
+
message_types: Dict[str, List[Dict[str, str]]],
|
|
367
|
+
message: Dict[str, Any],
|
|
368
|
+
signature: bytes
|
|
369
|
+
) -> str:
|
|
370
|
+
"""Verify an EIP-712 signature and recover the signer address.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
domain: EIP-712 domain separator
|
|
374
|
+
message_types: Type definitions for the message
|
|
375
|
+
message: Message data that was signed
|
|
376
|
+
signature: Signature bytes to verify
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Recovered signer address
|
|
380
|
+
"""
|
|
381
|
+
from eth_account.messages import encode_typed_data
|
|
382
|
+
|
|
383
|
+
structured_data = {
|
|
384
|
+
"types": {
|
|
385
|
+
"EIP712Domain": [
|
|
386
|
+
{"name": "name", "type": "string"},
|
|
387
|
+
{"name": "version", "type": "string"},
|
|
388
|
+
{"name": "chainId", "type": "uint256"},
|
|
389
|
+
{"name": "verifyingContract", "type": "address"}
|
|
390
|
+
],
|
|
391
|
+
**message_types
|
|
392
|
+
},
|
|
393
|
+
"domain": domain,
|
|
394
|
+
"primaryType": list(message_types.keys())[0] if message_types else "Message",
|
|
395
|
+
"message": message
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
encoded = encode_typed_data(full_message=structured_data)
|
|
399
|
+
return self.w3.eth.account.recover_message(encoded, signature=signature)
|