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 +4 -0
- oli/attestation/__init__.py +11 -0
- oli/attestation/offchain.py +273 -0
- oli/attestation/onchain.py +272 -0
- oli/attestation/utils_other.py +111 -0
- oli/attestation/utils_validator.py +200 -0
- oli/core.py +128 -0
- oli/data/__init__.py +4 -0
- oli/data/fetcher.py +111 -0
- oli/data/graphql.py +87 -0
- oli_python-0.1.0.dist-info/METADATA +117 -0
- oli_python-0.1.0.dist-info/RECORD +14 -0
- oli_python-0.1.0.dist-info/WHEEL +5 -0
- oli_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
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}")
|