rb125 commited on
Commit
c2db979
·
1 Parent(s): d2e404a

ENS integration

Browse files
Files changed (1) hide show
  1. cgae_engine/ens.py +281 -0
cgae_engine/ens.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CGAE ENS Integration — Real ENS names for AI agents on Sepolia.
3
+
4
+ Each agent gets a subname under a parent ENS name (e.g., gpt5.cgaeprotocol.eth)
5
+ with text records storing robustness scores, tier, wallet address, and 0G audit hash.
6
+
7
+ This uses the ENS NameWrapper + PublicResolver on Sepolia to:
8
+ 1. Create subnames for each agent (setSubnodeRecord)
9
+ 2. Set text records with robustness credentials
10
+ 3. Enable resolution: anyone can look up gpt5.cgaeprotocol.eth → get scores + wallet
11
+
12
+ Requirements:
13
+ - Parent ENS name registered on Sepolia (e.g., cgaeprotocol.eth)
14
+ - Parent name wrapped in NameWrapper
15
+ - Sepolia ETH for gas
16
+ - SEPOLIA_RPC_URL and PRIVATE_KEY in env
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import os
24
+ import re
25
+ from pathlib import Path
26
+ from typing import Optional
27
+
28
+ from eth_account import Account
29
+ from web3 import Web3
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Sepolia ENS contract addresses (from docs.ens.domains/learn/deployments)
34
+ ENS_REGISTRY = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
35
+ NAME_WRAPPER = "0x0635513f179D50A207757E05759CbD106d7dFcE8"
36
+ PUBLIC_RESOLVER = "0xE99638b40E4Fff0129D56f03b55b6bbC4BBE49b5"
37
+
38
+ # Minimal ABIs for the functions we need
39
+ NAME_WRAPPER_ABI = json.loads("""[
40
+ {
41
+ "inputs": [
42
+ {"name": "parentNode", "type": "bytes32"},
43
+ {"name": "label", "type": "string"},
44
+ {"name": "owner", "type": "address"},
45
+ {"name": "fuses", "type": "uint32"},
46
+ {"name": "expiry", "type": "uint64"}
47
+ ],
48
+ "name": "setSubnodeOwner",
49
+ "outputs": [{"name": "node", "type": "bytes32"}],
50
+ "stateMutability": "nonpayable",
51
+ "type": "function"
52
+ },
53
+ {
54
+ "inputs": [
55
+ {"name": "parentNode", "type": "bytes32"},
56
+ {"name": "label", "type": "string"},
57
+ {"name": "owner", "type": "address"},
58
+ {"name": "resolver", "type": "address"},
59
+ {"name": "ttl", "type": "uint64"},
60
+ {"name": "fuses", "type": "uint32"},
61
+ {"name": "expiry", "type": "uint64"}
62
+ ],
63
+ "name": "setSubnodeRecord",
64
+ "outputs": [{"name": "node", "type": "bytes32"}],
65
+ "stateMutability": "nonpayable",
66
+ "type": "function"
67
+ }
68
+ ]""")
69
+
70
+ RESOLVER_ABI = json.loads("""[
71
+ {
72
+ "inputs": [
73
+ {"name": "node", "type": "bytes32"},
74
+ {"name": "key", "type": "string"},
75
+ {"name": "value", "type": "string"}
76
+ ],
77
+ "name": "setText",
78
+ "outputs": [],
79
+ "stateMutability": "nonpayable",
80
+ "type": "function"
81
+ },
82
+ {
83
+ "inputs": [
84
+ {"name": "node", "type": "bytes32"},
85
+ {"name": "key", "type": "string"}
86
+ ],
87
+ "name": "text",
88
+ "outputs": [{"name": "", "type": "string"}],
89
+ "stateMutability": "view",
90
+ "type": "function"
91
+ },
92
+ {
93
+ "inputs": [
94
+ {"name": "node", "type": "bytes32"},
95
+ {"name": "coinType", "type": "uint256"}
96
+ ],
97
+ "name": "addr",
98
+ "outputs": [{"name": "", "type": "bytes"}],
99
+ "stateMutability": "view",
100
+ "type": "function"
101
+ }
102
+ ]""")
103
+
104
+
105
+ def namehash(name: str) -> bytes:
106
+ """Compute ENS namehash (EIP-137)."""
107
+ node = b"\x00" * 32
108
+ if name:
109
+ labels = name.split(".")
110
+ for label in reversed(labels):
111
+ label_hash = Web3.keccak(text=label)
112
+ node = Web3.keccak(node + label_hash)
113
+ return node
114
+
115
+
116
+ def _slugify(name: str) -> str:
117
+ s = name.lower().replace("_", "-").replace(" ", "-").replace(".", "-")
118
+ s = re.sub(r"[^a-z0-9-]", "", s)
119
+ return re.sub(r"-+", "-", s).strip("-") or "agent"
120
+
121
+
122
+ class ENSManager:
123
+ """
124
+ Manages ENS subnames for CGAE agents on Sepolia.
125
+
126
+ Creates subnames under a parent name and sets text records
127
+ with robustness scores, tier, and 0G audit provenance.
128
+ """
129
+
130
+ def __init__(
131
+ self,
132
+ parent_name: str = "cgaeprotocol.eth",
133
+ rpc_url: Optional[str] = None,
134
+ private_key: Optional[str] = None,
135
+ ):
136
+ self.parent_name = parent_name
137
+ self.rpc_url = rpc_url or os.getenv("SEPOLIA_RPC_URL", "https://ethereum-sepolia-rpc.publicnode.com")
138
+ self._key = private_key or os.getenv("PRIVATE_KEY")
139
+
140
+ self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
141
+
142
+ if self._key:
143
+ key = self._key if self._key.startswith("0x") else f"0x{self._key}"
144
+ self._account = Account.from_key(key)
145
+ else:
146
+ self._account = None
147
+
148
+ self.name_wrapper = self.w3.eth.contract(
149
+ address=Web3.to_checksum_address(NAME_WRAPPER), abi=NAME_WRAPPER_ABI
150
+ )
151
+ self.resolver = self.w3.eth.contract(
152
+ address=Web3.to_checksum_address(PUBLIC_RESOLVER), abi=RESOLVER_ABI
153
+ )
154
+ self.parent_node = namehash(parent_name)
155
+ self._subnames: dict[str, str] = {} # agent_id -> full ENS name
156
+
157
+ @property
158
+ def is_live(self) -> bool:
159
+ return self._account is not None
160
+
161
+ def create_subname(self, agent_id: str, model_name: str, owner: str) -> Optional[str]:
162
+ """
163
+ Create a subname like gpt5.cgaeprotocol.eth for an agent.
164
+ Returns the full ENS name or None on failure.
165
+ """
166
+ label = _slugify(model_name)
167
+ full_name = f"{label}.{self.parent_name}"
168
+
169
+ if not self.is_live:
170
+ logger.info(f" [ens] Dry run: would create {full_name}")
171
+ self._subnames[agent_id] = full_name
172
+ return full_name
173
+
174
+ try:
175
+ nonce = self.w3.eth.get_transaction_count(self._account.address)
176
+ # setSubnodeRecord creates the subname + sets resolver in one tx
177
+ tx = self.name_wrapper.functions.setSubnodeRecord(
178
+ self.parent_node,
179
+ label,
180
+ Web3.to_checksum_address(owner),
181
+ Web3.to_checksum_address(PUBLIC_RESOLVER),
182
+ 0, # ttl
183
+ 0, # fuses (no restrictions)
184
+ 2**64 - 1, # max expiry
185
+ ).build_transaction({
186
+ "from": self._account.address,
187
+ "nonce": nonce,
188
+ "gas": 300_000,
189
+ "gasPrice": self.w3.eth.gas_price,
190
+ "chainId": self.w3.eth.chain_id,
191
+ })
192
+ signed = self._account.sign_transaction(tx)
193
+ tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
194
+ self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
195
+
196
+ self._subnames[agent_id] = full_name
197
+ logger.info(f" [ens] Created {full_name} tx={tx_hash.hex()[:16]}…")
198
+ return full_name
199
+
200
+ except Exception as e:
201
+ logger.error(f" [ens] Failed to create {full_name}: {e}")
202
+ self._subnames[agent_id] = full_name # store anyway for display
203
+ return None
204
+
205
+ def set_text_records(
206
+ self,
207
+ agent_id: str,
208
+ records: dict[str, str],
209
+ ) -> int:
210
+ """
211
+ Set multiple text records on an agent's ENS subname.
212
+ Returns number of records successfully set.
213
+ """
214
+ full_name = self._subnames.get(agent_id)
215
+ if not full_name or not self.is_live:
216
+ return 0
217
+
218
+ node = namehash(full_name)
219
+ count = 0
220
+
221
+ for key, value in records.items():
222
+ try:
223
+ nonce = self.w3.eth.get_transaction_count(self._account.address)
224
+ tx = self.resolver.functions.setText(
225
+ node, key, value
226
+ ).build_transaction({
227
+ "from": self._account.address,
228
+ "nonce": nonce,
229
+ "gas": 100_000,
230
+ "gasPrice": self.w3.eth.gas_price,
231
+ "chainId": self.w3.eth.chain_id,
232
+ })
233
+ signed = self._account.sign_transaction(tx)
234
+ tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
235
+ self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
236
+ count += 1
237
+ except Exception as e:
238
+ logger.warning(f" [ens] setText({key}) failed for {full_name}: {e}")
239
+
240
+ if count:
241
+ logger.info(f" [ens] Set {count}/{len(records)} text records on {full_name}")
242
+ return count
243
+
244
+ def set_agent_credentials(
245
+ self,
246
+ agent_id: str,
247
+ tier: str,
248
+ cc: float, er: float, as_: float, ih: float,
249
+ wallet_address: str = "",
250
+ audit_hash: str = "",
251
+ family: str = "",
252
+ ) -> int:
253
+ """Set robustness credentials as ENS text records."""
254
+ records = {
255
+ "cgae.tier": tier,
256
+ "cgae.cc": f"{cc:.4f}",
257
+ "cgae.er": f"{er:.4f}",
258
+ "cgae.as": f"{as_:.4f}",
259
+ "cgae.ih": f"{ih:.4f}",
260
+ }
261
+ if wallet_address:
262
+ records["cgae.wallet"] = wallet_address
263
+ if audit_hash:
264
+ records["cgae.0g-audit-hash"] = audit_hash
265
+ if family:
266
+ records["cgae.family"] = family
267
+ return self.set_text_records(agent_id, records)
268
+
269
+ def resolve_text(self, ens_name: str, key: str) -> str:
270
+ """Read a text record from an ENS name."""
271
+ node = namehash(ens_name)
272
+ try:
273
+ return self.resolver.functions.text(node, key).call()
274
+ except Exception:
275
+ return ""
276
+
277
+ def get_agent_name(self, agent_id: str) -> Optional[str]:
278
+ return self._subnames.get(agent_id)
279
+
280
+ def all_subnames(self) -> dict[str, str]:
281
+ return dict(self._subnames)