oli-python 0.1.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.
oli/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from oli.core import OLI
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["OLI"]
@@ -0,0 +1,11 @@
1
+ from oli.attestation.onchain import OnchainAttestations
2
+ from oli.attestation.offchain import OffchainAttestations
3
+ from oli.attestation.utils_validator import UtilsValidator
4
+ from oli.attestation.utils_other import UtilsOther
5
+
6
+ __all__ = [
7
+ "OnchainAttestations",
8
+ "OffchainAttestations",
9
+ "UtilsValidator",
10
+ "UtilsOther"
11
+ ]
@@ -0,0 +1,273 @@
1
+ import time
2
+ import requests
3
+ import secrets
4
+ import json
5
+ from requests import Response
6
+
7
+ class OffchainAttestations:
8
+ def __init__(self, oli_client):
9
+ """
10
+ Initialize OffchainAttestations with an OLI client.
11
+
12
+ Args:
13
+ oli_client: The OLI client instance
14
+ """
15
+ self.oli = oli_client
16
+
17
+ def create_offchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", retry: int=4):
18
+ """
19
+ Create an offchain OLI label attestation for a contract.
20
+
21
+ Args:
22
+ address (str): The contract address to label
23
+ chain_id (str): Chain ID in CAIP-2 format where the address/contract resides
24
+ tags (dict): OLI compliant tags as a dict information (name, version, etc.)
25
+ ref_uid (str): Reference UID
26
+ retry (int): Number of retries for the API post request to EAS ipfs
27
+
28
+ Returns:
29
+ dict: API request response
30
+ """
31
+ # fix simple formatting errors in tags
32
+ tags = self.oli.validator.fix_simple_tags_formatting(tags)
33
+
34
+ # Check all necessary input parameters
35
+ self.oli.validator.check_label_correctness(address, chain_id, tags, ref_uid, auto_fix=False)
36
+
37
+ # Encode the label data
38
+ data = self.oli.utils_other.encode_label_data(chain_id, tags)
39
+
40
+ # Build the attestation
41
+ attestation = self.build_offchain_attestation(
42
+ recipient=address,
43
+ schema=self.oli.oli_label_pool_schema,
44
+ data=data,
45
+ ref_uid=ref_uid
46
+ )
47
+
48
+ # Post to the API & retry if status code is not 200
49
+ response = self.post_offchain_attestation(attestation)
50
+ n0 = retry
51
+ while response.status_code != 200 and retry > 0:
52
+ retry -= 1
53
+ time.sleep(2 ** (n0 - retry)) # exponential backoff
54
+ response = self.post_offchain_attestation(attestation)
55
+
56
+ # if it fails after all retries, raise an error
57
+ if response.status_code != 200:
58
+ raise Exception(f"Failed to submit offchain attestation to EAS API ipfs post endpoint after {n0} retries: {response.status_code} - {response.text}")
59
+
60
+ return response
61
+
62
+ def post_offchain_attestation(self, attestation: dict, filename: str="OLI.txt") -> Response:
63
+ """
64
+ Post API an attestation to the EAS API.
65
+
66
+ Args:
67
+ attestation (dict): The attestation package
68
+ filename (str): Custom filename
69
+
70
+ Returns:
71
+ dict: API response
72
+ """
73
+ # Convert numerical values to strings for JSON serialization
74
+ attestation["sig"]["message"]["time"] = str(attestation["sig"]["message"]["time"])
75
+ attestation["sig"]["message"]["expirationTime"] = str(attestation["sig"]["message"]["expirationTime"])
76
+ attestation["sig"]["domain"]["chainId"] = str(attestation["sig"]["domain"]["chainId"])
77
+
78
+ # Prepare payload for the API endpoint
79
+ payload = {
80
+ "filename": filename,
81
+ "textJson": json.dumps(attestation, separators=(',', ':'))
82
+ }
83
+
84
+ headers = {
85
+ "Content-Type": "application/json"
86
+ }
87
+
88
+ # Post the data to the API
89
+ response = requests.post(self.oli.eas_api_url, json=payload, headers=headers)
90
+ return response
91
+
92
+ def build_offchain_attestation(self, recipient: str, schema: str, data: str, ref_uid: str, revocable: bool=True, expiration_time: int=0) -> dict:
93
+ """
94
+ Build an offchain attestation with the given parameters.
95
+
96
+ Args:
97
+ recipient (str): Ethereum address of the contract to be labeled
98
+ schema (str): Schema hash
99
+ data (str): Hex-encoded data
100
+ ref_uid (str): Reference UID
101
+ revocable (bool): Whether the attestation is revocable
102
+ expiration_time (int): Expiration time in seconds since epoch
103
+
104
+ Returns:
105
+ dict: The signed attestation and UID
106
+ """
107
+ # Create a random salt
108
+ salt = f"0x{secrets.token_hex(32)}"
109
+
110
+ # Current time in seconds
111
+ current_time = int(time.time())
112
+
113
+ # Typed data for the attestation
114
+ typed_data = {
115
+ "version": 2,
116
+ "recipient": recipient,
117
+ "time": current_time,
118
+ "revocable": revocable,
119
+ "schema": schema,
120
+ "refUID": ref_uid,
121
+ "data": data,
122
+ "expirationTime": expiration_time,
123
+ "salt": salt,
124
+ }
125
+
126
+ # EIP-712 typed data format
127
+ types = {
128
+ "domain": {
129
+ "name": "EAS Attestation",
130
+ "version": "1.2.0",
131
+ "chainId": self.oli.rpc_chain_number,
132
+ "verifyingContract": self.oli.eas_address
133
+ },
134
+ "primaryType": "Attest",
135
+ "message": typed_data,
136
+ "types": {
137
+ "Attest": [
138
+ {"name": "version", "type": "uint16"},
139
+ {"name": "schema", "type": "bytes32"},
140
+ {"name": "recipient", "type": "address"},
141
+ {"name": "time", "type": "uint64"},
142
+ {"name": "expirationTime", "type": "uint64"},
143
+ {"name": "revocable", "type": "bool"},
144
+ {"name": "refUID", "type": "bytes32"},
145
+ {"name": "data", "type": "bytes"},
146
+ {"name": "salt", "type": "bytes32"}
147
+ ]
148
+ }
149
+ }
150
+
151
+ # Sign the message using the account
152
+ signed_message = self.oli.account.sign_typed_data(
153
+ domain_data=types["domain"],
154
+ message_types=types["types"],
155
+ message_data=typed_data
156
+ )
157
+
158
+ # Calculate the UID
159
+ attester = '0x0000000000000000000000000000000000000000' # for offchain UID calculation
160
+ uid = self.oli.utils_other.calculate_attestation_uid_v2(
161
+ schema, recipient, attester, current_time, data,
162
+ expiration_time, revocable, ref_uid, salt=salt
163
+ )
164
+ uid_hex = '0x' + uid.hex()
165
+
166
+ # Package the result
167
+ result = {
168
+ "sig": {
169
+ "domain": types["domain"],
170
+ "primaryType": types["primaryType"],
171
+ "types": types["types"],
172
+ "message": typed_data,
173
+ "uid": uid_hex,
174
+ "version": 2,
175
+ "signature": {
176
+ "r": hex(signed_message.r),
177
+ "s": hex(signed_message.s),
178
+ "v": signed_message.v
179
+ }
180
+ },
181
+ "signer": self.oli.address
182
+ }
183
+
184
+ return result
185
+
186
+ def revoke_attestation(self, uid_hex: str, gas_limit: int=200000):
187
+ """
188
+ Revoke an offchain attestation using its UID.
189
+
190
+ Args:
191
+ uid_hex (str): UID of the attestation to revoke (in hex format)
192
+ gas_limit (int): Gas limit for the transaction. If not set, defaults to 200000. Gas estimation is not possible for revoke transactions.
193
+
194
+ Returns:
195
+ str: Transaction hash
196
+ """
197
+ function = self.oli.eas.functions.revokeOffchain(self.oli.w3.to_bytes(hexstr=uid_hex))
198
+
199
+ # Define the transaction parameters
200
+ tx_params = {
201
+ 'chainId': self.oli.rpc_chain_number,
202
+ 'gasPrice': self.oli.w3.eth.gas_price,
203
+ 'nonce': self.oli.w3.eth.get_transaction_count(self.oli.address),
204
+ }
205
+
206
+ # Estimate gas if no limit provided
207
+ tx_params = self.oli.utils_other.estimate_gas_limit(function, tx_params, gas_limit)
208
+
209
+ # Build the transaction to revoke an attestation
210
+ transaction = function.build_transaction(tx_params)
211
+
212
+ # Sign the transaction
213
+ signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
214
+
215
+ # Send the transaction
216
+ try:
217
+ txn_hash = self.oli.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
218
+ except Exception as e:
219
+ raise Exception(f"Failed to send revoke transaction to mempool: {e}")
220
+
221
+ # Get the transaction receipt
222
+ txn_receipt = self.oli.w3.eth.wait_for_transaction_receipt(txn_hash)
223
+
224
+ # Check if the transaction was successful
225
+ if txn_receipt.status == 1:
226
+ return f"0x{txn_hash.hex()}"
227
+ else:
228
+ raise Exception(f"Transaction failed: {txn_receipt}")
229
+
230
+ def multi_revoke_attestations(self, uids: str, gas_limit: int=10000000):
231
+ """
232
+ Revoke multiple offchain attestations in a single transaction.
233
+
234
+ Args:
235
+ uids (list): List of UIDs to revoke (in hex format)
236
+ gas_limit (int): Gas limit for the transaction. If not set, defaults to 10000000. Gas estimation is not possible for revoke transactions.
237
+
238
+ Returns:
239
+ str: Transaction hash
240
+ int: Number of attestations revoked
241
+ """
242
+ revocation_data = []
243
+ for uid in uids:
244
+ revocation_data.append(self.oli.w3.to_bytes(hexstr=uid))
245
+ function = self.oli.eas.functions.multiRevokeOffchain(revocation_data)
246
+
247
+ # Define the transaction parameters
248
+ tx_params = {
249
+ 'chainId': self.oli.rpc_chain_number,
250
+ 'gasPrice': self.oli.w3.eth.gas_price,
251
+ 'nonce': self.oli.w3.eth.get_transaction_count(self.oli.address),
252
+ }
253
+
254
+ # Estimate gas if no limit provided
255
+ tx_params = self.oli.utils_other.estimate_gas_limit(function, tx_params, gas_limit)
256
+
257
+ # Build the transaction
258
+ transaction = function.build_transaction(tx_params)
259
+
260
+ # Sign the transaction
261
+ signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
262
+
263
+ # Send the transaction
264
+ txn_hash = self.oli.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
265
+
266
+ # Get the transaction receipt
267
+ txn_receipt = self.oli.w3.eth.wait_for_transaction_receipt(txn_hash)
268
+
269
+ # Check if the transaction was successful
270
+ if txn_receipt.status == 1:
271
+ return f"0x{txn_hash.hex()}", len(uids)
272
+ else:
273
+ raise Exception(f"Transaction failed: {txn_receipt}")
@@ -0,0 +1,272 @@
1
+ class OnchainAttestations:
2
+ def __init__(self, oli_client):
3
+ """
4
+ Initialize OnchainAttestations with an OLI client.
5
+
6
+ Args:
7
+ oli_client: The OLI client instance
8
+ """
9
+ self.oli = oli_client
10
+
11
+ def create_onchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", gas_limit: int=0) -> tuple[str, str]:
12
+ """
13
+ Create an onchain OLI label attestation for a contract.
14
+
15
+ Args:
16
+ address (str): The contract address to label
17
+ chain_id (str): Chain ID in CAIP-2 format where the address/contract resides
18
+ tags (dict): OLI compliant tags as a dict information (name, version, etc.)
19
+ ref_uid (str): Reference UID
20
+ gas_limit (int): Gas limit for the transaction. If set to 0, the function will estimate the gas limit.
21
+
22
+ Returns:
23
+ str: Transaction hash
24
+ str: UID of the attestation
25
+ """
26
+ # fix simple formatting errors in tags
27
+ tags = self.oli.validator.fix_simple_tags_formatting(tags)
28
+
29
+ # Check all necessary input parameters
30
+ self.oli.validator.check_label_correctness(address, chain_id, tags, ref_uid, auto_fix=False)
31
+
32
+ # Encode the label data
33
+ data = self.oli.utils_other.encode_label_data(chain_id, tags)
34
+
35
+ # Create the attestation
36
+ function = self.oli.eas.functions.attest({
37
+ 'schema': self.oli.w3.to_bytes(hexstr=self.oli.oli_label_pool_schema),
38
+ 'data': {
39
+ 'recipient': self.oli.w3.to_checksum_address(address),
40
+ 'expirationTime': 0,
41
+ 'revocable': True,
42
+ 'refUID': self.oli.w3.to_bytes(hexstr=ref_uid),
43
+ 'data': self.oli.w3.to_bytes(hexstr=data),
44
+ 'value': 0
45
+ }
46
+ })
47
+
48
+ # Define the transaction parameters
49
+ tx_params = {
50
+ 'chainId': self.oli.rpc_chain_number,
51
+ 'gasPrice': self.oli.w3.eth.gas_price,
52
+ 'nonce': self.oli.w3.eth.get_transaction_count(self.oli.address),
53
+ }
54
+
55
+ # Estimate gas if no limit provided
56
+ tx_params = self.oli.utils_other.estimate_gas_limit(function, tx_params, gas_limit)
57
+
58
+ # Build the transaction to attest one label
59
+ transaction = function.build_transaction(tx_params)
60
+
61
+ # Sign the transaction with the private key
62
+ signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
63
+
64
+ # Send the transaction
65
+ try:
66
+ txn_hash = self.oli.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
67
+ except Exception as e:
68
+ raise Exception(f"Failed to send transaction to mempool: {e}")
69
+
70
+ # Wait for the transaction receipt
71
+ txn_receipt = self.oli.w3.eth.wait_for_transaction_receipt(txn_hash)
72
+
73
+ # Check if the transaction was successful
74
+ if txn_receipt.status == 1:
75
+ return f"0x{txn_hash.hex()}", f"0x{txn_receipt.logs[0].data.hex()}"
76
+ else:
77
+ raise Exception(f"Transaction failed onchain: {txn_receipt}")
78
+
79
+ def create_multi_onchain_labels(self, labels: list, gas_limit: int=0) -> tuple[str, list]:
80
+ """
81
+ Batch submit OLI labels in one transaction.
82
+
83
+ Args:
84
+ labels (list): List of labels, containing dictionaries with 'address', 'tags', and 'chain_id' (, optional 'ref_uid')
85
+ address (str): The contract address to label
86
+ chain_id (str): Chain ID in CAIP-2 format where the address/contract resides
87
+ tags (dict): OLI compliant tags as a dict information (name, version, etc.)
88
+ ref_uid (str): Reference UID
89
+ gas_limit (int): Gas limit for one transaction to submit all labels passed, make sure to set it high enough for multiple attestations! If set to 0, the function will estimate the gas limit.
90
+
91
+ Returns:
92
+ str: Transaction hash
93
+ list: List of UID of the attestation
94
+ """
95
+ # Prepare the list of "data" requests
96
+ full_data = []
97
+ for label in labels:
98
+ # check if address, chain_id & tags are provided
99
+ if 'chain_id' not in label:
100
+ raise ValueError("chain_id must be provided for each label in CAIP-2 format (e.g., Base -> 'eip155:8453')")
101
+ elif 'address' not in label:
102
+ raise ValueError("An address must be provided for each label")
103
+ elif 'tags' not in label:
104
+ raise ValueError("tags dictionary must be provided for each label")
105
+
106
+ # fix simple formatting errors in tags
107
+ label['tags'] = self.oli.validator.fix_simple_tags_formatting(label['tags'])
108
+
109
+ # run checks on each label
110
+ self.oli.validator.check_label_correctness(label['address'], label['chain_id'], label['tags'], auto_fix=False)
111
+
112
+ # check if ref_uid is provided
113
+ if 'ref_uid' not in label:
114
+ label['ref_uid'] = "0x0000000000000000000000000000000000000000000000000000000000000000"
115
+ else:
116
+ self.oli.validator.checks_ref_uid(label['ref_uid'])
117
+
118
+ # ABI encode data for each attestation
119
+ data = self.oli.utils_other.encode_label_data(label['chain_id'], label['tags'])
120
+ full_data.append({
121
+ 'recipient': self.oli.w3.to_checksum_address(label['address']),
122
+ 'expirationTime': 0,
123
+ 'revocable': True,
124
+ 'refUID': self.oli.w3.to_bytes(hexstr=label['ref_uid']),
125
+ 'data': self.oli.w3.to_bytes(hexstr=data),
126
+ 'value': 0
127
+ })
128
+
129
+ # Create the multi-attestation request
130
+ multi_requests = [{
131
+ 'schema': self.oli.w3.to_bytes(hexstr=self.oli.oli_label_pool_schema),
132
+ 'data': full_data
133
+ }]
134
+
135
+ # Create the function call
136
+ function = self.oli.eas.functions.multiAttest(multi_requests)
137
+
138
+ # Define the transaction parameters
139
+ tx_params = {
140
+ 'chainId': self.oli.rpc_chain_number,
141
+ 'gasPrice': self.oli.w3.eth.gas_price,
142
+ 'nonce': self.oli.w3.eth.get_transaction_count(self.oli.address),
143
+ }
144
+
145
+ # Estimate gas if no limit provided
146
+ tx_params = self.oli.utils_other.estimate_gas_limit(function, tx_params, gas_limit)
147
+
148
+ # Build the transaction to revoke an attestation
149
+ transaction = function.build_transaction(tx_params)
150
+
151
+ # Sign the transaction with the private key
152
+ signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
153
+
154
+ # Send the transaction
155
+ try:
156
+ txn_hash = self.oli.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
157
+ except Exception as e:
158
+ raise Exception(f"Failed to send transaction to mempool: {e}")
159
+
160
+ # Wait for the transaction receipt
161
+ txn_receipt = self.oli.w3.eth.wait_for_transaction_receipt(txn_hash)
162
+
163
+ # Check if the transaction was successful
164
+ if txn_receipt.status != 1:
165
+ raise Exception(f"Transaction failed onchain: {txn_receipt}")
166
+
167
+ # log the UIDs of the attestations in a list
168
+ uids = ['0x' + log.data.hex() for log in txn_receipt.logs]
169
+
170
+ return f"0x{txn_hash.hex()}", uids
171
+
172
+ def revoke_attestation(self, uid_hex: str, gas_limit: int=200000) -> str:
173
+ """
174
+ Revoke an onchain attestation using its UID.
175
+
176
+ Args:
177
+ uid_hex (str): UID of the attestation to revoke (in hex format)
178
+ gas_limit (int): Gas limit for the transaction. If not set, defaults to 200000. Gas estimation is not possible for revoke transactions.
179
+
180
+ Returns:
181
+ str: Transaction hash
182
+ """
183
+ function = self.oli.eas.functions.revoke({
184
+ 'schema': self.oli.w3.to_bytes(hexstr=self.oli.oli_label_pool_schema),
185
+ 'data': {
186
+ 'uid': self.oli.w3.to_bytes(hexstr=uid_hex),
187
+ 'value': 0
188
+ }
189
+ })
190
+
191
+ # Define the transaction parameters
192
+ tx_params = {
193
+ 'chainId': self.oli.rpc_chain_number,
194
+ 'gasPrice': self.oli.w3.eth.gas_price,
195
+ 'nonce': self.oli.w3.eth.get_transaction_count(self.oli.address),
196
+ }
197
+
198
+ # Estimate gas if no limit provided
199
+ tx_params = self.oli.utils_other.estimate_gas_limit(function, tx_params, gas_limit)
200
+
201
+ # Build the transaction to revoke an attestation
202
+ transaction = function.build_transaction(tx_params)
203
+
204
+ # Sign the transaction
205
+ signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
206
+
207
+ # Send the transaction
208
+ try:
209
+ txn_hash = self.oli.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
210
+ except Exception as e:
211
+ raise Exception(f"Failed to send revoke transaction to mempool: {e}")
212
+
213
+ # Get the transaction receipt
214
+ txn_receipt = self.oli.w3.eth.wait_for_transaction_receipt(txn_hash)
215
+
216
+ # Check if the transaction was successful
217
+ if txn_receipt.status == 1:
218
+ return f"0x{txn_hash.hex()}"
219
+ else:
220
+ raise Exception(f"Transaction failed: {txn_receipt}")
221
+
222
+ def multi_revoke_attestations(self, uids: list, gas_limit: int=10000000) -> tuple[str, int]:
223
+ """
224
+ Revoke multiple onchain attestations in a single transaction.
225
+
226
+ Args:
227
+ uids (list): List of UIDs to revoke (in hex format)
228
+ gas_limit (int): Gas limit for the transaction. If not set, defaults to 10000000. Gas estimation is not possible for revoke transactions.
229
+
230
+ Returns:
231
+ str: Transaction hash
232
+ int: Number of attestations revoked
233
+ """
234
+ revocation_data = []
235
+ for uid in uids:
236
+ revocation_data.append({
237
+ 'uid': self.oli.w3.to_bytes(hexstr=uid),
238
+ 'value': 0
239
+ })
240
+ multi_requests = [{
241
+ 'schema': self.oli.w3.to_bytes(hexstr=self.oli.oli_label_pool_schema),
242
+ 'data': revocation_data
243
+ }]
244
+ function = self.oli.eas.functions.multiRevoke(multi_requests)
245
+
246
+ # Define the transaction parameters
247
+ tx_params = {
248
+ 'chainId': self.oli.rpc_chain_number,
249
+ 'gasPrice': self.oli.w3.eth.gas_price,
250
+ 'nonce': self.oli.w3.eth.get_transaction_count(self.oli.address),
251
+ }
252
+
253
+ # Estimate gas if no limit provided
254
+ tx_params = self.oli.utils_other.estimate_gas_limit(function, tx_params, gas_limit)
255
+
256
+ # Build the transaction
257
+ transaction = function.build_transaction(tx_params)
258
+
259
+ # Sign the transaction
260
+ signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
261
+
262
+ # Send the transaction
263
+ txn_hash = self.oli.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
264
+
265
+ # Get the transaction receipt
266
+ txn_receipt = self.oli.w3.eth.wait_for_transaction_receipt(txn_hash)
267
+
268
+ # Check if the transaction was successful
269
+ if txn_receipt.status == 1:
270
+ return f"0x{txn_hash.hex()}", len(uids)
271
+ else:
272
+ raise Exception(f"Transaction failed: {txn_receipt}")
@@ -0,0 +1,111 @@
1
+ import json
2
+ from eth_abi.abi import encode
3
+ import secrets
4
+ from web3 import Web3
5
+
6
+ class UtilsOther:
7
+ def __init__(self, oli_client):
8
+ """
9
+ Initialize the DataEncoder with an OLI client.
10
+
11
+ Args:
12
+ oli_client: The OLI client instance
13
+ """
14
+ self.oli = oli_client
15
+
16
+ def encode_label_data(self, chain_id: str, tags_json: dict) -> str:
17
+ """
18
+ Encode label data in the OLI format.
19
+
20
+ Args:
21
+ chain_id (str): Chain ID in CAIP-2 format of the label (e.g. 'eip155:8453')
22
+ tags_json (dict): Dictionary of tag data following the OLI format
23
+
24
+ Returns:
25
+ str: Hex-encoded ABI data
26
+ """
27
+ # Convert dict to JSON string if needed
28
+ if isinstance(tags_json, dict):
29
+ tags_json = json.dumps(tags_json)
30
+
31
+ # ABI encode the data
32
+ encoded_data = encode(['string', 'string'], [chain_id, tags_json])
33
+ return f"0x{encoded_data.hex()}"
34
+
35
+ def estimate_gas_limit(self, function, tx_params: dict, gas_limit: int) -> dict:
36
+ """
37
+ Estimate gas for a transaction.
38
+
39
+ Args:
40
+ function: The function to estimate gas for
41
+ tx_params (dict): Transaction parameters
42
+ gas_limit (int): Gas limit
43
+
44
+ Returns:
45
+ tx_params (dict): Transaction parameters with estimated 'gas' field
46
+ """
47
+ try:
48
+ if gas_limit == 0:
49
+ # Estimate gas with a buffer (e.g., 10% more than the estimate)
50
+ estimated_gas = function.estimate_gas(tx_params)
51
+ tx_params["gas"] = int(estimated_gas * 1.1) # Add 10% buffer
52
+ else:
53
+ tx_params["gas"] = gas_limit
54
+ except Exception as e:
55
+ tx_params["gas"] = 10000000 # Default fallback
56
+ return tx_params
57
+
58
+ def calculate_attestation_uid_v2(self, schema: str, recipient: str, attester: str, timestamp: int, data: str, expiration_time: int=0, revocable: bool=True, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", bump: int=0, salt: str=None) -> bytes:
59
+ """
60
+ Calculate the UID for an offchain attestation (v2).
61
+
62
+ Args:
63
+ schema (str): Schema hash
64
+ recipient (str): Recipient address
65
+ attester (str): Attester address
66
+ timestamp (int): Timestamp
67
+ data (str): Attestation data
68
+ expiration_time (int): Expiration time
69
+ revocable (bool): Whether attestation is revocable
70
+ ref_uid (str): Reference UID
71
+ bump (int): Bump value
72
+ salt (str): Salt value
73
+
74
+ Returns:
75
+ bytes: The calculated UID
76
+ """
77
+ # Generate salt if not provided
78
+ if salt is None:
79
+ salt = f"0x{secrets.token_hex(32)}"
80
+
81
+ # Version
82
+ version = 2
83
+ version_bytes = version.to_bytes(2, byteorder='big')
84
+
85
+ # Handle schema formatting
86
+ if not schema.startswith('0x'):
87
+ schema = '0x' + schema
88
+ schema_utf8_bytes = schema.encode('utf-8')
89
+ schema_bytes = schema_utf8_bytes
90
+
91
+ # Convert values to bytes
92
+ recipient_bytes = Web3.to_bytes(hexstr=recipient)
93
+ attester_bytes = Web3.to_bytes(hexstr=attester)
94
+ timestamp_bytes = timestamp.to_bytes(8, byteorder='big')
95
+ expiration_bytes = expiration_time.to_bytes(8, byteorder='big')
96
+ revocable_bytes = bytes([1]) if revocable else bytes([0])
97
+ ref_uid_bytes = Web3.to_bytes(hexstr=ref_uid)
98
+ data_bytes = Web3.to_bytes(hexstr=data)
99
+ salt_bytes = Web3.to_bytes(hexstr=salt)
100
+ bump_bytes = bump.to_bytes(4, byteorder='big')
101
+
102
+ # Pack all values
103
+ packed_data = (
104
+ version_bytes + schema_bytes + recipient_bytes + attester_bytes +
105
+ timestamp_bytes + expiration_bytes + revocable_bytes + ref_uid_bytes +
106
+ data_bytes + salt_bytes + bump_bytes
107
+ )
108
+
109
+ # Calculate keccak256 hash
110
+ uid = Web3.keccak(packed_data)
111
+ return uid