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.
@@ -0,0 +1,192 @@
1
+ """
2
+ Web3 integration layer for smart contract interactions.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from typing import Any, Dict, List, Optional, Tuple, Union
9
+
10
+ try:
11
+ from web3 import Web3
12
+ from web3.contract import Contract
13
+ from eth_account import Account
14
+ from eth_account.signers.base import BaseAccount
15
+ except ImportError:
16
+ raise ImportError(
17
+ "Web3 dependencies not installed. Install with: pip install web3 eth-account"
18
+ )
19
+
20
+
21
+ class Web3Client:
22
+ """Web3 client for interacting with ERC-8004 smart contracts."""
23
+
24
+ def __init__(
25
+ self,
26
+ rpc_url: str,
27
+ private_key: Optional[str] = None,
28
+ account: Optional[BaseAccount] = None,
29
+ ):
30
+ """Initialize Web3 client."""
31
+ self.rpc_url = rpc_url
32
+ self.w3 = Web3(Web3.HTTPProvider(rpc_url))
33
+ if not self.w3.is_connected():
34
+ raise ConnectionError("Failed to connect to Ethereum node")
35
+
36
+ if account:
37
+ self.account = account
38
+ elif private_key:
39
+ self.account = Account.from_key(private_key)
40
+ else:
41
+ # Read-only mode - no account
42
+ self.account = None
43
+
44
+ self.chain_id = self.w3.eth.chain_id
45
+
46
+ def get_contract(self, address: str, abi: List[Dict[str, Any]]) -> Contract:
47
+ """Get contract instance."""
48
+ return self.w3.eth.contract(address=address, abi=abi)
49
+
50
+ def call_contract(
51
+ self,
52
+ contract: Contract,
53
+ method_name: str,
54
+ *args,
55
+ **kwargs
56
+ ) -> Any:
57
+ """Call a contract method (view/pure)."""
58
+ method = getattr(contract.functions, method_name)
59
+ return method(*args, **kwargs).call()
60
+
61
+ def transact_contract(
62
+ self,
63
+ contract: Contract,
64
+ method_name: str,
65
+ *args,
66
+ gas_limit: Optional[int] = None,
67
+ gas_price: Optional[int] = None,
68
+ max_fee_per_gas: Optional[int] = None,
69
+ max_priority_fee_per_gas: Optional[int] = None,
70
+ **kwargs
71
+ ) -> str:
72
+ """Execute a contract transaction."""
73
+ if not self.account:
74
+ raise ValueError("Cannot execute transaction: SDK is in read-only mode. Provide a signer to enable write operations.")
75
+
76
+ method = getattr(contract.functions, method_name)
77
+
78
+ # Build transaction with proper nonce management
79
+ # Use 'pending' to get the next nonce including pending transactions
80
+ nonce = self.w3.eth.get_transaction_count(self.account.address, 'pending')
81
+ tx = method(*args, **kwargs).build_transaction({
82
+ 'from': self.account.address,
83
+ 'nonce': nonce,
84
+ })
85
+
86
+ # Add gas settings
87
+ if gas_limit:
88
+ tx['gas'] = gas_limit
89
+ if gas_price:
90
+ tx['gasPrice'] = gas_price
91
+ if max_fee_per_gas:
92
+ tx['maxFeePerGas'] = max_fee_per_gas
93
+ if max_priority_fee_per_gas:
94
+ tx['maxPriorityFeePerGas'] = max_priority_fee_per_gas
95
+
96
+ # Sign and send
97
+ signed_tx = self.w3.eth.account.sign_transaction(tx, self.account.key)
98
+ tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction if hasattr(signed_tx, 'rawTransaction') else signed_tx.raw_transaction)
99
+
100
+ return tx_hash.hex()
101
+
102
+ def wait_for_transaction(self, tx_hash: str, timeout: int = 60) -> Dict[str, Any]:
103
+ """Wait for transaction to be mined."""
104
+ return self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
105
+
106
+ def get_events(
107
+ self,
108
+ contract: Contract,
109
+ event_name: str,
110
+ from_block: int = 0,
111
+ to_block: Optional[int] = None,
112
+ argument_filters: Optional[Dict[str, Any]] = None
113
+ ) -> List[Dict[str, Any]]:
114
+ """Get contract events."""
115
+ if to_block is None:
116
+ to_block = self.w3.eth.block_number
117
+
118
+ event_filter = contract.events[event_name].create_filter(
119
+ fromBlock=from_block,
120
+ toBlock=to_block,
121
+ argument_filters=argument_filters or {}
122
+ )
123
+
124
+ return event_filter.get_all_entries()
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
+ def signMessage(self, message: bytes) -> bytes:
143
+ """Sign a message with the account's private key."""
144
+ # Create a SignableMessage from the raw bytes
145
+ from eth_account.messages import encode_defunct
146
+ signableMessage = encode_defunct(message)
147
+ signedMessage = self.account.sign_message(signableMessage)
148
+ return signedMessage.signature
149
+
150
+ def recoverAddress(self, message: bytes, signature: bytes) -> str:
151
+ """Recover address from message and signature."""
152
+ from eth_account.messages import encode_defunct
153
+ signable_message = encode_defunct(message)
154
+ return self.w3.eth.account.recover_message(signable_message, signature=signature)
155
+
156
+ def keccak256(self, data: bytes) -> bytes:
157
+ """Compute Keccak-256 hash."""
158
+ return self.w3.keccak(data)
159
+
160
+ def to_checksum_address(self, address: str) -> str:
161
+ """Convert address to checksum format."""
162
+ return self.w3.to_checksum_address(address)
163
+
164
+ def normalize_address(self, address: str) -> str:
165
+ """Normalize address to lowercase for consistent storage and comparison.
166
+
167
+ Ethereum addresses are case-insensitive but EIP-55 checksum addresses
168
+ use mixed case. For storage and comparison purposes, we normalize to
169
+ lowercase to avoid case-sensitivity issues.
170
+
171
+ Args:
172
+ address: Ethereum address (with or without checksum)
173
+
174
+ Returns:
175
+ Address in lowercase format
176
+ """
177
+ # Remove 0x prefix if present, convert to lowercase, re-add prefix
178
+ if address.startswith("0x") or address.startswith("0X"):
179
+ return "0x" + address[2:].lower()
180
+ return address.lower()
181
+
182
+ def is_address(self, address: str) -> bool:
183
+ """Check if string is a valid Ethereum address."""
184
+ return self.w3.is_address(address)
185
+
186
+ def get_balance(self, address: str) -> int:
187
+ """Get ETH balance of an address."""
188
+ return self.w3.eth.get_balance(address)
189
+
190
+ def get_transaction_count(self, address: str) -> int:
191
+ """Get transaction count (nonce) of an address."""
192
+ return self.w3.eth.get_transaction_count(address)