Skip to content

Commit ea34aeb

Browse files
Index cyber reverse name (HemeraProtocol#173)
1 parent 07e4f82 commit ea34aeb

File tree

9 files changed

+280
-0
lines changed

9 files changed

+280
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ resource/hemera.ini
5050
sync_record
5151
alembic.ini
5252
!indexer/modules/custom/hemera_ens/abi/*.json
53+
!indexer/modules/custom/cyber_id/abi/*.json

indexer/modules/custom/cyber_id/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"address": "0x79502da131357333d61c39b7411d01df54591961",
3+
"abi":[{"inputs":[{"internalType":"contract ENS","name":"_ens","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"controller","type":"address"},{"indexed":false,"internalType":"bool","name":"enabled","type":"bool"}],"name":"ControllerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract NameResolver","name":"resolver","type":"address"}],"name":"DefaultResolverChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"ReverseClaimed","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"claim","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"}],"name":"claimForAddr","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"}],"name":"claimWithResolver","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"controllers","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"defaultResolver","outputs":[{"internalType":"contract NameResolver","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ens","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"node","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"controller","type":"address"},{"internalType":"bool","name":"enabled","type":"bool"}],"name":"setController","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"resolver","type":"address"}],"name":"setDefaultResolver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"setName","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"string","name":"name","type":"string"}],"name":"setNameForAddr","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]
4+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import json
2+
import os
3+
4+
5+
def get_absolute_path(relative_path):
6+
current_dir = os.path.dirname(os.path.abspath(__file__))
7+
absolute_path = os.path.join(current_dir, relative_path)
8+
return absolute_path
9+
10+
11+
abi_map = {}
12+
13+
relative_path = "abi"
14+
absolute_path = get_absolute_path(relative_path)
15+
fs = os.listdir(absolute_path)
16+
for a_f in fs:
17+
with open(os.path.join(absolute_path, a_f), "r") as data_file:
18+
dic = json.load(data_file)
19+
abi_map[dic["address"].lower()] = json.dumps(dic["abi"])
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from dataclasses import dataclass
2+
from datetime import datetime
3+
from typing import Optional
4+
5+
from numpy.distutils.fcompiler import none
6+
7+
from indexer.domain import FilterData
8+
9+
10+
@dataclass
11+
class CyberAddressD(FilterData):
12+
address: str
13+
reverse_node: str
14+
name: str
15+
block_number: int
16+
17+
18+
@dataclass
19+
class CyberIDRegisterD(FilterData):
20+
label: str
21+
token_id: int
22+
node: str
23+
cost: int
24+
block_number: int
25+
registration: datetime
26+
27+
28+
@dataclass
29+
class CyberAddressChangedD(FilterData):
30+
node: str
31+
address: str
32+
block_number: int
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import logging
2+
from itertools import groupby
3+
from typing import List
4+
5+
from web3 import Web3
6+
7+
from indexer.domain.log import Log
8+
from indexer.domain.transaction import Transaction
9+
from indexer.executors.batch_work_executor import BatchWorkExecutor
10+
from indexer.jobs import FilterTransactionDataJob
11+
from indexer.modules.custom.cyber_id.cyber_abi import abi_map
12+
from indexer.modules.custom.cyber_id.cyber_domain import CyberAddressChangedD, CyberAddressD, CyberIDRegisterD
13+
from indexer.modules.custom.cyber_id.utils import get_node, get_reverse_node
14+
from indexer.specification.specification import TopicSpecification, TransactionFilterByLogs
15+
16+
logger = logging.getLogger(__name__)
17+
18+
CyberIdReverseRegistrarContractAddress = "0x79502da131357333d61c39b7411d01df54591961"
19+
CyberIdPublicResolverContractAddress = "0xfb2f304c1fcd6b053ee033c03293616d5121944b"
20+
CyberIdTokenContractAddress = "0xc137be6b59e824672aada673e55cf4d150669af8"
21+
NameChangedTopic = "0xb7d29e911041e8d9b843369e890bcb72c9388692ba48b65ac54e7214c4c348f7"
22+
RegisterTopic = "0xa50d98082663c2b716ab4f8b6b2a51fcaed7eae222cd3d74b19de4691ede728a"
23+
AddressChangedTopic = "0x65412581168e88a1e60c6459d7f44ae83ad0832e670826c05a4e2476b57af752"
24+
25+
26+
class ExportCyberIDJob(FilterTransactionDataJob):
27+
dependency_types = [Transaction]
28+
output_types = [CyberAddressD, CyberIDRegisterD, CyberAddressChangedD]
29+
able_to_reorg = True
30+
31+
def __init__(self, **kwargs):
32+
super().__init__(**kwargs)
33+
34+
self._batch_work_executor = BatchWorkExecutor(
35+
kwargs["batch_size"],
36+
kwargs["max_workers"],
37+
job_name=self.__class__.__name__,
38+
)
39+
self._is_batch = kwargs["batch_size"] > 1
40+
self._filters = kwargs.get("filters", [])
41+
self.contract_object_map = {}
42+
self.func_name_map = {}
43+
self.w3 = Web3(Web3.HTTPProvider(self._web3.provider.endpoint_uri))
44+
for ad_lower in abi_map:
45+
abi = abi_map[ad_lower]
46+
contract = self.w3.eth.contract(address=Web3.to_checksum_address(ad_lower), abi=abi)
47+
self.contract_object_map[ad_lower] = contract
48+
for contract_address, contract in self.contract_object_map.items():
49+
if not contract:
50+
continue
51+
functions = [abi for abi in contract.abi if abi["type"] == "function"]
52+
for function in functions:
53+
sig = self.get_function_signature(function)
54+
self.func_name_map[sig[0:10]] = function
55+
56+
def get_filter(self):
57+
58+
return [
59+
TransactionFilterByLogs(
60+
[
61+
TopicSpecification(
62+
addresses=[CyberIdPublicResolverContractAddress, CyberIdTokenContractAddress],
63+
topics=[NameChangedTopic, RegisterTopic],
64+
)
65+
]
66+
),
67+
]
68+
69+
def _collect(self, **kwargs):
70+
transactions: List[Transaction] = self._data_buff.get(Transaction.type(), [])
71+
for transaction in transactions:
72+
if transaction.to_address.lower() == CyberIdReverseRegistrarContractAddress:
73+
func_name = self.func_name_map.get(transaction.input[0:10], {}).get("name")
74+
if func_name == "setNameForAddr":
75+
decoded_input = self.decode_transaction(transaction)
76+
cyber_address = CyberAddressD(
77+
address=decoded_input[1].get("addr").lower(),
78+
name=decoded_input[1].get("name"),
79+
block_number=transaction.block_number,
80+
reverse_node=get_reverse_node(decoded_input[1].get("addr")),
81+
)
82+
self._collect_item(cyber_address.type(), cyber_address)
83+
if func_name == "setName":
84+
decoded_input = self.decode_transaction(transaction)
85+
cyber_address = CyberAddressD(
86+
address=transaction.from_address.lower(),
87+
name=decoded_input[1].get("name"),
88+
block_number=transaction.block_number,
89+
reverse_node=get_reverse_node(transaction.from_address),
90+
)
91+
self._collect_item(cyber_address.type(), cyber_address)
92+
logs: List[Log] = self._data_buff.get(Log.type(), [])
93+
for log in logs:
94+
if log.address.lower() == CyberIdTokenContractAddress and log.topic0 == RegisterTopic:
95+
decoded_data = self.w3.codec.decode(["string", "uint256"], bytes.fromhex(log.data[2:]))
96+
cid = decoded_data[0]
97+
cyber_address = CyberIDRegisterD(
98+
label=cid,
99+
token_id=log.topic3,
100+
cost=int(decoded_data[1]),
101+
block_number=log.block_number,
102+
node=get_node(cid + ".cyber"),
103+
registration=log.block_timestamp,
104+
)
105+
self._collect_item(cyber_address.type(), cyber_address)
106+
if log.address.lower() == CyberIdPublicResolverContractAddress and log.topic0 == AddressChangedTopic:
107+
decoded_data = self.w3.codec.decode(["uint256", "bytes"], bytes.fromhex(log.data[2:]))
108+
address_change_d = CyberAddressChangedD(
109+
node=log.topic1, address="0x" + decoded_data[1].hex(), block_number=log.block_number
110+
)
111+
self._collect_item(address_change_d.type(), address_change_d)
112+
113+
def _process(self, **kwargs):
114+
cyber_addresses = self._data_buff.get(CyberAddressD.type(), [])
115+
cyber_addresses.sort(key=lambda x: (x.address, x.block_number))
116+
self._data_buff[CyberAddressD.type()] = [
117+
list(group)[-1] for key, group in groupby(cyber_addresses, key=lambda x: x.address)
118+
]
119+
120+
address_changes = self._data_buff.get(CyberAddressChangedD.type(), [])
121+
address_changes.sort(key=lambda x: (x.node, x.block_number))
122+
self._data_buff[CyberAddressChangedD.type()] = [
123+
list(group)[-1] for key, group in groupby(address_changes, key=lambda x: x.node)
124+
]
125+
126+
def decode_transaction(self, transaction):
127+
if not transaction.to_address:
128+
return None
129+
con = self.contract_object_map[transaction.to_address]
130+
decoded_input = con.decode_function_input(transaction.input)
131+
return decoded_input
132+
133+
def get_function_signature(self, function_abi):
134+
name = function_abi["name"]
135+
inputs = [input["type"] for input in function_abi["inputs"]]
136+
signature = f"{name}({','.join(inputs)})"
137+
sig = self.w3.to_hex(Web3.keccak(text=signature))
138+
return sig

indexer/modules/custom/cyber_id/models/__init__.py

Whitespace-only changes.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from sqlalchemy import Column, func
2+
from sqlalchemy.dialects.postgresql import BIGINT, BYTEA, NUMERIC, TIMESTAMP, VARCHAR
3+
4+
from common.models import HemeraModel
5+
from indexer.modules.custom.hemera_ens.models.af_ens_node_current import ens_general_converter
6+
7+
8+
class CyberAddress(HemeraModel):
9+
__tablename__ = "cyber_address"
10+
11+
address = Column(BYTEA, primary_key=True)
12+
name = Column(VARCHAR)
13+
reverse_node = Column(BYTEA)
14+
block_number = Column(BIGINT)
15+
create_time = Column(TIMESTAMP, server_default=func.now())
16+
update_time = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
17+
18+
@staticmethod
19+
def model_domain_mapping():
20+
return [
21+
{
22+
"domain": "CyberAddressD",
23+
"conflict_do_update": True,
24+
"update_strategy": "EXCLUDED.block_number > cyber_address.block_number",
25+
"converter": ens_general_converter,
26+
}
27+
]
28+
29+
30+
class CyberIDRecord(HemeraModel):
31+
__tablename__ = "cyber_id_record"
32+
33+
node = Column(BYTEA, primary_key=True)
34+
token_id = Column(NUMERIC(100))
35+
label = Column(VARCHAR)
36+
registration = Column(TIMESTAMP)
37+
address = Column(BYTEA)
38+
block_number = Column(BIGINT)
39+
cost = Column(NUMERIC(100))
40+
41+
create_time = Column(TIMESTAMP, server_default=func.now())
42+
update_time = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
43+
44+
@staticmethod
45+
def model_domain_mapping():
46+
return [
47+
{
48+
"domain": "CyberIDRegisterD",
49+
"conflict_do_update": None,
50+
"update_strategy": None,
51+
"converter": ens_general_converter,
52+
},
53+
{
54+
"domain": "CyberAddressChangedD",
55+
"conflict_do_update": True,
56+
"update_strategy": "EXCLUDED.block_number >= cyber_id_record.block_number",
57+
"converter": ens_general_converter,
58+
},
59+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from ens.auto import ns
2+
from ens.constants import EMPTY_SHA3_BYTES
3+
from ens.utils import Web3, address_to_reverse_domain, is_empty_name, normal_name_to_hash, normalize_name
4+
from hexbytes import HexBytes
5+
6+
7+
def get_reverse_node(address):
8+
address = address_to_reverse_domain(address)
9+
return ns.namehash(address)
10+
11+
12+
def label_to_hash(label: str) -> HexBytes:
13+
if "." in label:
14+
raise ValueError(f"Cannot generate hash for label {label!r} with a '.'")
15+
return Web3().keccak(text=label)
16+
17+
18+
def get_node(name):
19+
node = EMPTY_SHA3_BYTES
20+
if not is_empty_name(name):
21+
labels = name.split(".")
22+
for label in reversed(labels):
23+
label_hash = label_to_hash(label)
24+
assert isinstance(label_hash, bytes)
25+
assert isinstance(node, bytes)
26+
node = Web3().keccak(node + label_hash)
27+
return node.hex()

0 commit comments

Comments
 (0)