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.
@@ -0,0 +1,200 @@
1
+ class UtilsValidator:
2
+ def __init__(self, oli_client):
3
+ """
4
+ Initialize the DataValidator with an OLI client.
5
+
6
+ Args:
7
+ oli_client: The OLI client instance
8
+ """
9
+ self.oli = oli_client
10
+ self.allowed_prefixes = [
11
+ 'eip155:', # Ethereum and EVM-compatible chains
12
+ 'solana:', # Solana
13
+ 'tron:', # TRON
14
+ 'stellar:', # Stellar
15
+ 'bip122:', # Bitcoin
16
+ 'SN_' # Starknet
17
+ ]
18
+
19
+ def fix_simple_tags_formatting(self, tags: dict) -> dict:
20
+ """
21
+ Fix basic formatting in the tags dictionary. This includes:
22
+ - Ensuring all tag_ids and their value are lowercase
23
+ - Booling values are converted from strings to booleans
24
+ - Removing leading/trailing whitespace from string values
25
+ - Checksum address (string(42)) tags
26
+
27
+ Args:
28
+ tags (dict): Dictionary of tags
29
+
30
+ Returns:
31
+ dict: Formatted tags
32
+ """
33
+ # Convert tag_ids to lowercase
34
+ tags = {k.lower(): v for k, v in tags.items()}
35
+
36
+ # Convert all tag_values to lower case & strip whitespaces, then single boolean values from strings to booleans
37
+ for k, v in tags.items():
38
+ if isinstance(v, str):
39
+ tags[k] = v.strip().lower()
40
+ if tags[k] == 'true':
41
+ tags[k] = True
42
+ elif tags[k] == 'false':
43
+ tags[k] = False
44
+ elif isinstance(v, list):
45
+ tags[k] = [i.strip().lower() if isinstance(i, str) else i for i in v]
46
+
47
+ # Checksum address (string(42)) and transaction hash (string(66)) tags
48
+ for k, v in tags.items():
49
+ if k in self.oli.tag_definitions and self.oli.tag_definitions[k]['type'] == 'string(42)':
50
+ tags[k] = self.oli.w3.to_checksum_address(v)
51
+
52
+ return tags
53
+
54
+ def check_label_correctness(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", auto_fix: bool=True) -> bool:
55
+ """
56
+ Check if the label is compliant with the OLI Data Model. See OLI Github documentation for more details: https://github.com/openlabelsinitiative/OLI
57
+
58
+ Args:
59
+ address (str): Address to check
60
+ chain_id (str): Chain ID to check
61
+ tags (dict): Tags to check
62
+ ref_uid (str): Reference UID to check
63
+ auto_fix (bool): If True, will attempt to fix the label automatically using the fix_simple_tags_formatting function
64
+
65
+ Returns:
66
+ bool: True if the label is correct, False otherwise
67
+ """
68
+ # basic checks
69
+ self.checks_address(address)
70
+ self.checks_chain_id(chain_id)
71
+ self.checks_tags(tags, auto_fix=auto_fix)
72
+ self.checks_ref_uid(ref_uid)
73
+ return True
74
+
75
+ def checks_chain_id(self, chain_id: str) -> bool:
76
+ """
77
+ Check if chain_id for a label is in CAIP-2 format.
78
+
79
+ Args:
80
+ chain_id (str): Chain ID to check
81
+
82
+ Returns:
83
+ bool: True if correct, False otherwise
84
+ """
85
+ # Check if the chain_id starts with any of the allowed prefixes
86
+ for prefix in self.allowed_prefixes:
87
+ if chain_id.startswith(prefix):
88
+ # For eip155, further validate that the rest is a number or 'any'
89
+ if prefix == 'eip155:':
90
+ rest = chain_id[len(prefix):]
91
+ if rest.isdigit():
92
+ return True
93
+ elif rest == 'any':
94
+ print("Please ensure the label is accurate and consistent across all EVM chains before setting chain_id = 'eip155:any'.")
95
+ return True
96
+ else:
97
+ print(f"Invalid eip155 chain_id format: {chain_id}")
98
+ raise ValueError("For eip155 chains, format must be 'eip155:' followed by a number or 'any'")
99
+ return True
100
+
101
+ # If we get here, the chain_id didn't match any allowed format
102
+ print(f"Unsupported chain ID format: {chain_id}")
103
+ raise ValueError("Chain ID must be in CAIP-2 format (e.g., Base -> 'eip155:8453'), see this guide on CAIP-2: https://docs.portalhq.io/resources/chain-id-formatting")
104
+
105
+ def checks_address(self, address: str) -> bool:
106
+ """
107
+ Check if address is a valid Ethereum address.
108
+
109
+ Args:
110
+ address (str): Address to check
111
+
112
+ Returns:
113
+ bool: True if correct, False otherwise
114
+ """
115
+ if self.oli.w3.is_address(address):
116
+ return True
117
+ else:
118
+ print(address)
119
+ raise ValueError("Address must be a valid Ethereum address in hex format")
120
+
121
+ def checks_tags(self, tags: dict, auto_fix: bool=False) -> bool:
122
+ """
123
+ Check if tags are in the correct format.
124
+
125
+ Args:
126
+ tags (dict): Tags to check
127
+
128
+ Returns:
129
+ bool: True if correct, False otherwise
130
+ """
131
+ # Check if tags is a dictionary
132
+ if isinstance(tags, dict):
133
+ if auto_fix:
134
+ tags = self.fix_simple_tags_formatting(tags)
135
+ else:
136
+ pass
137
+ else:
138
+ print(tags)
139
+ raise ValueError("Tags must be a dictionary with OLI compliant tags (e.g., {'contract_name': 'example', 'is_eoa': True})")
140
+
141
+ # Check each tag_id in the dictionary
142
+ for tag_id in tags.keys():
143
+
144
+ # Check if the tag_id is in the official OLI tag list
145
+ if tag_id not in self.oli.tag_ids:
146
+ print(f"WARNING: Tag tag_id '{tag_id}' is not an official OLI tag. Please check the 'oli.tag_definitions' or https://github.com/openlabelsinitiative/OLI/blob/main/1_data_model/tags/tag_definitions.yml.")
147
+
148
+ # Check if the tag_id is in the correct format. So far implemented [boolean, string, integer, list, float, string(42), string(66), date (YYYY-MM-DD HH:MM:SS)]
149
+ else:
150
+ if self.oli.tag_definitions[tag_id]['type'] == 'boolean' and not isinstance(tags[tag_id], bool):
151
+ print(f"WARNING: Tag value for {tag_id} must be a boolean (True/False).")
152
+ elif self.oli.tag_definitions[tag_id]['type'] == 'string' and not isinstance(tags[tag_id], str):
153
+ print(f"WARNING: Tag value for {tag_id} must be a string.")
154
+ elif self.oli.tag_definitions[tag_id]['type'] == 'integer' and not isinstance(tags[tag_id], int):
155
+ print(f"WARNING: Tag value for {tag_id} must be an integer.")
156
+ elif self.oli.tag_definitions[tag_id]['type'] == 'float' and not isinstance(tags[tag_id], float):
157
+ print(f"WARNING: Tag value for {tag_id} must be a float.")
158
+ elif self.oli.tag_definitions[tag_id]['type'] == 'list' and not isinstance(tags[tag_id], list):
159
+ print(f"WARNING: Tag value for {tag_id} must be a list.")
160
+ elif self.oli.tag_definitions[tag_id]['type'] == 'string(42)' and not self.oli.w3.is_address(tags[tag_id]):
161
+ print(f"WARNING: Tag value for {tag_id} must be a valid Ethereum address string with '0x'.")
162
+ elif self.oli.tag_definitions[tag_id]['type'] == 'string(66)' and not (len(tags[tag_id]) == 66 and tags[tag_id].startswith('0x')):
163
+ print(f"WARNING: Tag value for {tag_id} must be a valid hex string with '0x' prefix and 64 hex characters (66 characters total).")
164
+ elif self.oli.tag_definitions[tag_id]['type'] == 'date (YYYY-MM-DD HH:MM:SS)' and not isinstance(tags[tag_id], str):
165
+ print(f"WARNING: Tag value for {tag_id} must be a string in the format 'YYYY-MM-DD HH:MM:SS'.")
166
+
167
+ # Check if the value is in the value set
168
+ if tag_id in self.oli.tag_value_sets:
169
+ # single value
170
+ if tags[tag_id] not in self.oli.tag_value_sets[tag_id] and not isinstance(tags[tag_id], list):
171
+ print(f"WARNING: Invalid tag value for {tag_id}: '{tags[tag_id]}'")
172
+ if len(self.oli.tag_value_sets[tag_id]) < 100:
173
+ print(f"Please use one of the following values for {tag_id}: {self.oli.tag_value_sets[tag_id]}")
174
+ else:
175
+ print(f"Please use a valid value from the predefined value_set for {tag_id}: {self.oli.tag_definitions[tag_id]['value_set']}")
176
+ # list of values
177
+ elif tags[tag_id] not in self.oli.tag_value_sets[tag_id] and isinstance(tags[tag_id], list):
178
+ for i in tags[tag_id]:
179
+ if i not in self.oli.tag_value_sets[tag_id]:
180
+ print(f"WARNING: Invalid tag value for {tag_id}: {i}")
181
+ if len(self.oli.tag_value_sets[tag_id]) < 100:
182
+ print(f"Please use a list of values from the predefined value_set for {tag_id}: {self.oli.tag_value_sets[tag_id]}")
183
+ else:
184
+ print(f"Please use a list of values from the predefined value_set for {tag_id}: {self.oli.tag_definitions[tag_id]['value_set']}")
185
+
186
+ def checks_ref_uid(self, ref_uid: str) -> bool:
187
+ """
188
+ Check if ref_uid is a valid UID.
189
+
190
+ Args:
191
+ ref_uid (str): Reference UID to check
192
+
193
+ Returns:
194
+ bool: True if correct, throws error otherwise
195
+ """
196
+ if ref_uid.startswith('0x') and len(ref_uid) == 66:
197
+ return True
198
+ else:
199
+ print(ref_uid)
200
+ raise ValueError("Ref_uid must be a valid UID in hex format, leave empty if not used")
oli/core.py ADDED
@@ -0,0 +1,128 @@
1
+ from web3 import Web3
2
+ import eth_account
3
+ from eth_keys import keys
4
+ from requests import Response
5
+
6
+ from oli.attestation.utils_validator import UtilsValidator
7
+ from oli.attestation.utils_other import UtilsOther
8
+ from oli.attestation.onchain import OnchainAttestations
9
+ from oli.attestation.offchain import OffchainAttestations
10
+ from oli.data.fetcher import DataFetcher
11
+ from oli.data.graphql import GraphQLClient
12
+
13
+ class OLI:
14
+ def __init__(self, private_key: str, is_production: bool=True, custom_rpc_url: str=None) -> None:
15
+ """
16
+ Initialize the OLI API client.
17
+
18
+ Args:
19
+ private_key (str): The private key to sign attestations
20
+ is_production (bool): Whether to use production or testnet
21
+ """
22
+ print("Initializing OLI API client...")
23
+
24
+ # Set network based on environment
25
+ if is_production:
26
+ self.rpc = "https://mainnet.base.org"
27
+ self.graphql = "https://base.easscan.org/graphql"
28
+ self.rpc_chain_number = 8453
29
+ self.eas_api_url = "https://base.easscan.org/offchain/store"
30
+ self.eas_address = "0x4200000000000000000000000000000000000021" # EAS contract address on mainnet
31
+ else:
32
+ self.rpc = "https://sepolia.base.org"
33
+ self.graphql = "https://base-sepolia.easscan.org/graphql"
34
+ self.rpc_chain_number = 84532
35
+ self.eas_api_url = "https://base-sepolia.easscan.org/offchain/store"
36
+ self.eas_address = "0x4200000000000000000000000000000000000021" # EAS contract address on testnet
37
+
38
+ # Use provided RPC endpoint if specified
39
+ if custom_rpc_url is not None:
40
+ self.rpc = custom_rpc_url
41
+
42
+ # Initialize Web3 and account
43
+ self.w3 = Web3(Web3.HTTPProvider(self.rpc))
44
+ if not self.w3.is_connected():
45
+ raise Exception("Failed to connect to the Ethereum node")
46
+
47
+ # Convert the hex private key to the proper key object
48
+ self.private_key = private_key
49
+ if private_key.startswith('0x'):
50
+ private_key_bytes = private_key[2:]
51
+ else:
52
+ private_key_bytes = private_key
53
+ private_key_obj = keys.PrivateKey(bytes.fromhex(private_key_bytes))
54
+
55
+ # Create account from private key
56
+ self.account = eth_account.Account.from_key(private_key_obj)
57
+ self.address = self.account.address
58
+
59
+ # Label Pool Schema for OLI
60
+ self.oli_label_pool_schema = '0xb763e62d940bed6f527dd82418e146a904e62a297b8fa765c9b3e1f0bc6fdd68'
61
+
62
+ # Load EAS ABI
63
+ self.eas_abi = '[{"inputs": [],"stateMutability": "nonpayable","type": "constructor"},{"inputs": [],"name": "AccessDenied","type": "error"},{"inputs": [],"name": "AlreadyRevoked","type": "error"},{"inputs": [],"name": "AlreadyRevokedOffchain","type": "error"},{"inputs": [],"name": "AlreadyTimestamped","type": "error"},{"inputs": [],"name": "DeadlineExpired","type": "error"},{"inputs": [],"name": "InsufficientValue","type": "error"},{"inputs": [],"name": "InvalidAttestation","type": "error"},{"inputs": [],"name": "InvalidAttestations","type": "error"},{"inputs": [],"name": "InvalidExpirationTime","type": "error"},{"inputs": [],"name": "InvalidLength","type": "error"},{"inputs": [],"name": "InvalidNonce","type": "error"},{"inputs": [],"name": "InvalidOffset","type": "error"},{"inputs": [],"name": "InvalidRegistry","type": "error"},{"inputs": [],"name": "InvalidRevocation","type": "error"},{"inputs": [],"name": "InvalidRevocations","type": "error"},{"inputs": [],"name": "InvalidSchema","type": "error"},{"inputs": [],"name": "InvalidSignature","type": "error"},{"inputs": [],"name": "InvalidVerifier","type": "error"},{"inputs": [],"name": "Irrevocable","type": "error"},{"inputs": [],"name": "NotFound","type": "error"},{"inputs": [],"name": "NotPayable","type": "error"},{"inputs": [],"name": "WrongSchema","type": "error"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "address","name": "recipient","type": "address"},{"indexed": true,"internalType": "address","name": "attester","type": "address"},{"indexed": false,"internalType": "bytes32","name": "uid","type": "bytes32"},{"indexed": true,"internalType": "bytes32","name": "schemaUID","type": "bytes32"}],"name": "Attested","type": "event"},{"anonymous": false,"inputs": [{"indexed": false,"internalType": "uint256","name": "oldNonce","type": "uint256"},{"indexed": false,"internalType": "uint256","name": "newNonce","type": "uint256"}],"name": "NonceIncreased","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "address","name": "recipient","type": "address"},{"indexed": true,"internalType": "address","name": "attester","type": "address"},{"indexed": false,"internalType": "bytes32","name": "uid","type": "bytes32"},{"indexed": true,"internalType": "bytes32","name": "schemaUID","type": "bytes32"}],"name": "Revoked","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "address","name": "revoker","type": "address"},{"indexed": true,"internalType": "bytes32","name": "data","type": "bytes32"},{"indexed": true,"internalType": "uint64","name": "timestamp","type": "uint64"}],"name": "RevokedOffchain","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "bytes32","name": "data","type": "bytes32"},{"indexed": true,"internalType": "uint64","name": "timestamp","type": "uint64"}],"name": "Timestamped","type": "event"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "address","name": "recipient","type": "address"},{"internalType": "uint64","name": "expirationTime","type": "uint64"},{"internalType": "bool","name": "revocable","type": "bool"},{"internalType": "bytes32","name": "refUID","type": "bytes32"},{"internalType": "bytes","name": "data","type": "bytes"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct AttestationRequestData","name": "data","type": "tuple"}],"internalType": "struct AttestationRequest","name": "request","type": "tuple"}],"name": "attest","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "payable","type": "function"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "address","name": "recipient","type": "address"},{"internalType": "uint64","name": "expirationTime","type": "uint64"},{"internalType": "bool","name": "revocable","type": "bool"},{"internalType": "bytes32","name": "refUID","type": "bytes32"},{"internalType": "bytes","name": "data","type": "bytes"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct AttestationRequestData","name": "data","type": "tuple"},{"components": [{"internalType": "uint8","name": "v","type": "uint8"},{"internalType": "bytes32","name": "r","type": "bytes32"},{"internalType": "bytes32","name": "s","type": "bytes32"}],"internalType": "struct Signature","name": "signature","type": "tuple"},{"internalType": "address","name": "attester","type": "address"},{"internalType": "uint64","name": "deadline","type": "uint64"}],"internalType": "struct DelegatedAttestationRequest","name": "delegatedRequest","type": "tuple"}],"name": "attestByDelegation","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "payable","type": "function"},{"inputs": [],"name": "getAttestTypeHash","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "pure","type": "function"},{"inputs": [{"internalType": "bytes32","name": "uid","type": "bytes32"}],"name": "getAttestation","outputs": [{"components": [{"internalType": "bytes32","name": "uid","type": "bytes32"},{"internalType": "bytes32","name": "schema","type": "bytes32"},{"internalType": "uint64","name": "time","type": "uint64"},{"internalType": "uint64","name": "expirationTime","type": "uint64"},{"internalType": "uint64","name": "revocationTime","type": "uint64"},{"internalType": "bytes32","name": "refUID","type": "bytes32"},{"internalType": "address","name": "recipient","type": "address"},{"internalType": "address","name": "attester","type": "address"},{"internalType": "bool","name": "revocable","type": "bool"},{"internalType": "bytes","name": "data","type": "bytes"}],"internalType": "struct Attestation","name": "","type": "tuple"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "getDomainSeparator","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "getName","outputs": [{"internalType": "string","name": "","type": "string"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "account","type": "address"}],"name": "getNonce","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "revoker","type": "address"},{"internalType": "bytes32","name": "data","type": "bytes32"}],"name": "getRevokeOffchain","outputs": [{"internalType": "uint64","name": "","type": "uint64"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "getRevokeTypeHash","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "pure","type": "function"},{"inputs": [],"name": "getSchemaRegistry","outputs": [{"internalType": "contract ISchemaRegistry","name": "","type": "address"}],"stateMutability": "pure","type": "function"},{"inputs": [{"internalType": "bytes32","name": "data","type": "bytes32"}],"name": "getTimestamp","outputs": [{"internalType": "uint64","name": "","type": "uint64"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "uint256","name": "newNonce","type": "uint256"}],"name": "increaseNonce","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "bytes32","name": "uid","type": "bytes32"}],"name": "isAttestationValid","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "view","type": "function"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "address","name": "recipient","type": "address"},{"internalType": "uint64","name": "expirationTime","type": "uint64"},{"internalType": "bool","name": "revocable","type": "bool"},{"internalType": "bytes32","name": "refUID","type": "bytes32"},{"internalType": "bytes","name": "data","type": "bytes"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct AttestationRequestData[]","name": "data","type": "tuple[]"}],"internalType": "struct MultiAttestationRequest[]","name": "multiRequests","type": "tuple[]"}],"name": "multiAttest","outputs": [{"internalType": "bytes32[]","name": "","type": "bytes32[]"}],"stateMutability": "payable","type": "function"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "address","name": "recipient","type": "address"},{"internalType": "uint64","name": "expirationTime","type": "uint64"},{"internalType": "bool","name": "revocable","type": "bool"},{"internalType": "bytes32","name": "refUID","type": "bytes32"},{"internalType": "bytes","name": "data","type": "bytes"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct AttestationRequestData[]","name": "data","type": "tuple[]"},{"components": [{"internalType": "uint8","name": "v","type": "uint8"},{"internalType": "bytes32","name": "r","type": "bytes32"},{"internalType": "bytes32","name": "s","type": "bytes32"}],"internalType": "struct Signature[]","name": "signatures","type": "tuple[]"},{"internalType": "address","name": "attester","type": "address"},{"internalType": "uint64","name": "deadline","type": "uint64"}],"internalType": "struct MultiDelegatedAttestationRequest[]","name": "multiDelegatedRequests","type": "tuple[]"}],"name": "multiAttestByDelegation","outputs": [{"internalType": "bytes32[]","name": "","type": "bytes32[]"}],"stateMutability": "payable","type": "function"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "bytes32","name": "uid","type": "bytes32"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct RevocationRequestData[]","name": "data","type": "tuple[]"}],"internalType": "struct MultiRevocationRequest[]","name": "multiRequests","type": "tuple[]"}],"name": "multiRevoke","outputs": [],"stateMutability": "payable","type": "function"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "bytes32","name": "uid","type": "bytes32"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct RevocationRequestData[]","name": "data","type": "tuple[]"},{"components": [{"internalType": "uint8","name": "v","type": "uint8"},{"internalType": "bytes32","name": "r","type": "bytes32"},{"internalType": "bytes32","name": "s","type": "bytes32"}],"internalType": "struct Signature[]","name": "signatures","type": "tuple[]"},{"internalType": "address","name": "revoker","type": "address"},{"internalType": "uint64","name": "deadline","type": "uint64"}],"internalType": "struct MultiDelegatedRevocationRequest[]","name": "multiDelegatedRequests","type": "tuple[]"}],"name": "multiRevokeByDelegation","outputs": [],"stateMutability": "payable","type": "function"},{"inputs": [{"internalType": "bytes32[]","name": "data","type": "bytes32[]"}],"name": "multiRevokeOffchain","outputs": [{"internalType": "uint64","name": "","type": "uint64"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "bytes32[]","name": "data","type": "bytes32[]"}],"name": "multiTimestamp","outputs": [{"internalType": "uint64","name": "","type": "uint64"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "bytes32","name": "uid","type": "bytes32"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct RevocationRequestData","name": "data","type": "tuple"}],"internalType": "struct RevocationRequest","name": "request","type": "tuple"}],"name": "revoke","outputs": [],"stateMutability": "payable","type": "function"},{"inputs": [{"components": [{"internalType": "bytes32","name": "schema","type": "bytes32"},{"components": [{"internalType": "bytes32","name": "uid","type": "bytes32"},{"internalType": "uint256","name": "value","type": "uint256"}],"internalType": "struct RevocationRequestData","name": "data","type": "tuple"},{"components": [{"internalType": "uint8","name": "v","type": "uint8"},{"internalType": "bytes32","name": "r","type": "bytes32"},{"internalType": "bytes32","name": "s","type": "bytes32"}],"internalType": "struct Signature","name": "signature","type": "tuple"},{"internalType": "address","name": "revoker","type": "address"},{"internalType": "uint64","name": "deadline","type": "uint64"}],"internalType": "struct DelegatedRevocationRequest","name": "delegatedRequest","type": "tuple"}],"name": "revokeByDelegation","outputs": [],"stateMutability": "payable","type": "function"},{"inputs": [{"internalType": "bytes32","name": "data","type": "bytes32"}],"name": "revokeOffchain","outputs": [{"internalType": "uint64","name": "","type": "uint64"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "bytes32","name": "data","type": "bytes32"}],"name": "timestamp","outputs": [{"internalType": "uint64","name": "","type": "uint64"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"name": "version","outputs": [{"internalType": "string","name": "","type": "string"}],"stateMutability": "view","type": "function"}]'
64
+
65
+ # Initialize EAS contract
66
+ self.eas = self.w3.eth.contract(address=self.eas_address, abi=self.eas_abi)
67
+
68
+ # Initialize components
69
+ self.data_fetcher = DataFetcher(self)
70
+ self.tag_definitions = self.data_fetcher.get_OLI_tags()
71
+ self.tag_ids = list(self.tag_definitions.keys())
72
+ self.tag_value_sets = self.data_fetcher.get_OLI_value_sets()
73
+
74
+ # Initialize validator
75
+ self.validator = UtilsValidator(self)
76
+
77
+ # Initialize other utilities
78
+ self.utils_other = UtilsOther(self)
79
+
80
+ # Initialize onchain and offchain attestations
81
+ self.onchain = OnchainAttestations(self)
82
+ self.offchain = OffchainAttestations(self)
83
+
84
+ # Initialize GraphQL client
85
+ self.graphql_client = GraphQLClient(self)
86
+
87
+ print("...OLI client initialized successfully.")
88
+
89
+ # Expose onchain attestation methods
90
+ def create_onchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", gas_limit: int=0) -> tuple[str, str]:
91
+ return self.onchain.create_onchain_label(address, chain_id, tags, ref_uid, gas_limit)
92
+
93
+ def create_multi_onchain_labels(self, labels: list, gas_limit: int=0) -> tuple[str, list]:
94
+ return self.onchain.create_multi_onchain_labels(labels, gas_limit)
95
+
96
+ # Expose offchain attestation methods
97
+ def create_offchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", retry: int=4) -> Response:
98
+ return self.offchain.create_offchain_label(address, chain_id, tags, ref_uid, retry)
99
+
100
+ # Expose revocation methods
101
+ def revoke_attestation(self, uid_hex: str, onchain: bool, gas_limit: int=200000) -> str:
102
+ if onchain:
103
+ return self.onchain.revoke_attestation(uid_hex, gas_limit)
104
+ else:
105
+ return self.offchain.revoke_attestation(uid_hex, gas_limit)
106
+
107
+ def multi_revoke_attestations(self, uids: str, onchain: bool, gas_limit: int=10000000) -> str:
108
+ if onchain:
109
+ return self.onchain.multi_revoke_attestations(uids, gas_limit)
110
+ else:
111
+ return self.offchain.multi_revoke_attestations(uids, gas_limit)
112
+
113
+ # Expose query methods
114
+ def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None) -> dict:
115
+ return self.graphql_client.graphql_query_attestations(address, attester, timeCreated, revocationTime)
116
+
117
+ def get_full_raw_export_parquet(self, file_path: str="raw_labels.parquet") -> str:
118
+ return self.data_fetcher.get_full_raw_export_parquet(file_path)
119
+
120
+ def get_full_decoded_export_parquet(self, file_path: str="decoded_labels.parquet") -> str:
121
+ return self.data_fetcher.get_full_decoded_export_parquet(file_path)
122
+
123
+ # Expose validation methods
124
+ def check_label_correctness(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", auto_fix: bool=True) -> bool:
125
+ return self.validator.check_label_correctness(address, chain_id, tags, ref_uid, auto_fix)
126
+
127
+ def fix_simple_tags_formatting(self, tags: dict) -> dict:
128
+ return self.validator.fix_simple_tags_formatting(tags)
oli/data/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from oli.data.graphql import GraphQLClient
2
+ from oli.data.fetcher import DataFetcher
3
+
4
+ __all__ = ["GraphQLClient", "DataFetcher"]
oli/data/fetcher.py ADDED
@@ -0,0 +1,111 @@
1
+ import requests
2
+ import yaml
3
+
4
+ class DataFetcher:
5
+ def __init__(self, oli_client):
6
+ """
7
+ Initialize the DataFetcher with an OLI client.
8
+
9
+ Args:
10
+ oli_client: The OLI client instance
11
+ """
12
+ self.oli = oli_client
13
+
14
+ def get_OLI_tags(self):
15
+ """
16
+ Get latest OLI tags from OLI Github repo.
17
+
18
+ Returns:
19
+ dict: Dictionary of official OLI tags
20
+ """
21
+ url = "https://raw.githubusercontent.com/openlabelsinitiative/OLI/refs/heads/main/1_data_model/tags/tag_definitions.yml"
22
+ response = requests.get(url)
23
+ if response.status_code == 200:
24
+ y = yaml.safe_load(response.text)
25
+ y = {i['tag_id']: i for i in y['tags']}
26
+ return y
27
+ else:
28
+ raise Exception(f"Failed to fetch OLI tags from Github: {response.status_code} - {response.text}")
29
+
30
+ def get_OLI_value_sets(self) -> dict:
31
+ """
32
+ Get latest value sets for OLI tags.
33
+
34
+ Returns:
35
+ dict: Dictionary of value sets with tag_id as key
36
+ """
37
+ value_sets = {}
38
+
39
+ # value sets from self.oli.tag_definitions (must be a list)
40
+ additional_value_sets = {i['tag_id']: i['value_set'] for i in self.oli.tag_definitions.values() if 'value_set' in i}
41
+ for tag_id, value_set in additional_value_sets.items():
42
+ if isinstance(value_set, list):
43
+ # convert all string values to lowercase and keep the rest as is
44
+ value_set = [i.lower() if isinstance(i, str) else i for i in value_set]
45
+ value_sets[tag_id] = value_set
46
+
47
+ # value set for owner_project
48
+ url = "https://api.growthepie.xyz/v1/labels/projects.json"
49
+ response = requests.get(url)
50
+ if response.status_code == 200:
51
+ y = yaml.safe_load(response.text)
52
+ value_sets["owner_project"] = [i[0] for i in y['data']['data']]
53
+ value_sets["owner_project"] = [i.lower() if isinstance(i, str) else i for i in value_sets["owner_project"]]
54
+ else:
55
+ raise Exception(f"Failed to fetch owner_project value set from grwothepie projects api: {response.status_code} - {response.text}")
56
+
57
+ # value set for usage_category
58
+ url = "https://raw.githubusercontent.com/openlabelsinitiative/OLI/refs/heads/main/1_data_model/tags/valuesets/usage_category.yml"
59
+ response = requests.get(url)
60
+ if response.status_code == 200:
61
+ y = yaml.safe_load(response.text)
62
+ value_sets['usage_category'] = [i['category_id'] for i in y['categories']]
63
+ value_sets['usage_category'] = [i.lower() if isinstance(i, str) else i for i in value_sets['usage_category']]
64
+ else:
65
+ raise Exception(f"Failed to fetch usage_category value set from OLI Github: {response.status_code} - {response.text}")
66
+
67
+ return value_sets
68
+
69
+ def get_full_raw_export_parquet(self, file_path: str="raw_labels.parquet") -> str:
70
+ """
71
+ Downloads the full raw export of all attestations in Parquet format.
72
+
73
+ Args:
74
+ file_path (str): Path where the file will be saved. Defaults to "raw_labels.parquet".
75
+
76
+ Returns:
77
+ str: Path to the downloaded Parquet file
78
+ """
79
+ url = "https://api.growthepie.xyz/v1/oli/labels_raw.parquet"
80
+
81
+ response = requests.get(url, stream=True)
82
+ if response.status_code == 200:
83
+ with open(file_path, 'wb') as f:
84
+ f.write(response.content)
85
+ print(f"Downloaded and saved: {file_path}")
86
+ return file_path
87
+ else:
88
+ print(f"Failed to download {url}. Status code: {response.status_code}")
89
+ return None
90
+
91
+ def get_full_decoded_export_parquet(self, file_path: str="decoded_labels.parquet") -> str:
92
+ """
93
+ Downloads the full decoded export of all attestations in Parquet format.
94
+
95
+ Args:
96
+ file_path (str): Path where the file will be saved. Defaults to "decoded_labels.parquet".
97
+
98
+ Returns:
99
+ str: Path to the downloaded Parquet file
100
+ """
101
+ url = "https://api.growthepie.xyz/v1/oli/labels_decoded.parquet"
102
+
103
+ response = requests.get(url, stream=True)
104
+ if response.status_code == 200:
105
+ with open(file_path, 'wb') as f:
106
+ f.write(response.content)
107
+ print(f"Downloaded and saved: {file_path}")
108
+ return file_path
109
+ else:
110
+ print(f"Failed to download {url}. Status code: {response.status_code}")
111
+ return None
oli/data/graphql.py ADDED
@@ -0,0 +1,87 @@
1
+ import requests
2
+
3
+ class GraphQLClient:
4
+ def __init__(self, oli_client):
5
+ """
6
+ Initialize the GraphQLClient with an OLI client.
7
+
8
+ Args:
9
+ oli_client: The OLI client instance
10
+ """
11
+ self.oli = oli_client
12
+
13
+ def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None) -> dict:
14
+ """
15
+ Queries attestations from the EAS GraphQL API based on the specified filters.
16
+
17
+ Args:
18
+ address (str, optional): Ethereum address of the labeled contract
19
+ attester (str, optional): Ethereum address of the attester
20
+ timeCreated (int, optional): Filter for attestations created after this timestamp
21
+ revocationTime (int, optional): Filter for attestations with revocation time >= this timestamp
22
+
23
+ Returns:
24
+ dict: JSON response containing matching attestation data
25
+ """
26
+ query = """
27
+ query Attestations($take: Int, $where: AttestationWhereInput, $orderBy: [AttestationOrderByWithRelationInput!]) {
28
+ attestations(take: $take, where: $where, orderBy: $orderBy) {
29
+ attester
30
+ decodedDataJson
31
+ expirationTime
32
+ id
33
+ ipfsHash
34
+ isOffchain
35
+ recipient
36
+ refUID
37
+ revocable
38
+ revocationTime
39
+ revoked
40
+ time
41
+ timeCreated
42
+ txid
43
+ }
44
+ }
45
+ """
46
+
47
+ variables = {
48
+ "where": {
49
+ "schemaId": {
50
+ "equals": self.oli.oli_label_pool_schema
51
+ }
52
+ },
53
+ "orderBy": [
54
+ {
55
+ "timeCreated": "desc"
56
+ }
57
+ ]
58
+ }
59
+
60
+ # Add address to where clause if not None
61
+ if address is not None:
62
+ variables["where"]["recipient"] = {"equals": address}
63
+
64
+ # Add attester to where clause if not None
65
+ if attester is not None:
66
+ variables["where"]["attester"] = {"equals": attester}
67
+
68
+ # Add timeCreated to where clause if not None, ensuring it's an int
69
+ if timeCreated is not None:
70
+ timeCreated = int(timeCreated)
71
+ variables["where"]["timeCreated"] = {"gt": timeCreated}
72
+
73
+ # Add revocationTime to where clause if not None, ensuring it's an int
74
+ if revocationTime is not None:
75
+ revocationTime = int(revocationTime)
76
+ variables["where"]["revocationTime"] = {"gte": revocationTime}
77
+
78
+ headers = {
79
+ "Content-Type": "application/json"
80
+ }
81
+
82
+ response = requests.post(self.oli.graphql, json={"query": query, "variables": variables}, headers=headers)
83
+
84
+ if response.status_code == 200:
85
+ return response.json()
86
+ else:
87
+ raise Exception(f"GraphQL query failed with status code {response.status_code}: {response.text}")