New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

oli-python

Package Overview
Dependencies
Maintainers
2
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

oli-python - pypi Package Compare versions

Comparing version
1.2.1
to
2.0.0
+438
oli/data/api.py
import requests
import yaml
class API:
def __init__(self, oli_client, api_key: str=None):
"""
Initialize the DataFetcher with an OLI client.
Args:
oli_client: The OLI client instance
"""
self.oli = oli_client
self.api_url = "https://api.openlabelsinitiative.org"
self.api_key = api_key
### OLI Tags and Value Sets maintained in Github ###
def get_OLI_tags(self) -> dict:
"""
Get latest OLI tags from OLI Github repo.
Returns:
dict: Dictionary of official OLI tags
"""
url = "https://raw.githubusercontent.com/openlabelsinitiative/OLI/refs/heads/main/1_label_schema/tags/tag_definitions.yml"
try:
response = requests.get(url)
if response.status_code == 200:
y = yaml.safe_load(response.text)
y = {i['tag_id']: i for i in y['tags']}
return y
else:
print(f"Failed to fetch OLI tags from Github, are you offline? (or are you not using the latest version of the oli-python package? 'pip install --upgrade oli-python'): {response.status_code}")
except Exception as e:
print(f"Failed to fetch OLI tags from Github (are you using the latest version of the oli-python package? 'pip install --upgrade oli-python'): {e}")
def get_OLI_value_sets(self) -> dict:
"""
Get latest value sets for OLI tags.
Returns:
dict: Dictionary of value sets with tag_id as key
"""
value_sets = {}
# Extract value sets from tag definitions (must be a list)
for tag_def in self.oli.tag_definitions.values():
if 'schema' not in tag_def:
continue
schema = tag_def['schema']
tag_id = tag_def['tag_id']
value_set = None
# Get enum from direct schema or array items
if 'enum' in schema:
value_set = schema['enum']
elif (schema.get('type') == 'array' and
'items' in schema and
'enum' in schema['items']):
value_set = schema['items']['enum']
# Process and add to value_sets
if value_set and isinstance(value_set, list):
value_sets[tag_id] = [i.lower() if isinstance(i, str) else i for i in value_set]
# value set for owner_project
url = "https://api.growthepie.com/v1/labels/projects.json"
try:
response = requests.get(url)
if response.status_code == 200:
y = yaml.safe_load(response.text)
value_sets["owner_project"] = [i[0] for i in y['data']['data']]
value_sets["owner_project"] = [i.lower() if isinstance(i, str) else i for i in value_sets["owner_project"]]
else:
print(f"Failed to fetch owner_project value set from growthepie projects api, are you offline? (or are you not using the latest version of the oli-python package? 'pip install --upgrade oli-python'): {response.status_code}")
except Exception as e:
print(f"Failed to fetch owner_project value set from growthepie projects api (are you using the latest version of the oli-python package? 'pip install --upgrade oli-python'): {e}")
# value set for usage_category
url = "https://raw.githubusercontent.com/openlabelsinitiative/OLI/refs/heads/main/1_label_schema/tags/valuesets/usage_category.yml"
try:
response = requests.get(url)
if response.status_code == 200:
y = yaml.safe_load(response.text)
value_sets['usage_category'] = [i['category_id'] for i in y['categories']]
value_sets['usage_category'] = [i.lower() if isinstance(i, str) else i for i in value_sets['usage_category']]
else:
print(f"Failed to fetch usage_category value set from OLI Github, are you offline? (or are you not using the latest version of the oli-python package? 'pip install --upgrade oli-python'): {response.status_code}")
except Exception as e:
print(f"Failed to fetch usage_category value set from OLI Github (are you using the latest version of the oli-python package? 'pip install --upgrade oli-python'): {e}")
return value_sets
### Parquet exports of OLI Label Pool by growthepie ###
def get_full_raw_export_parquet(self, file_path: str="raw_labels.parquet") -> str:
"""
Downloads the full raw export of all labels in the OLI Label Pool as a Parquet file.
Args:
file_path (str): Path where the file will be saved. Defaults to "raw_labels.parquet".
Returns:
str: Path to the downloaded Parquet file
"""
url = "https://api.growthepie.com/v1/oli/labels_raw.parquet"
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(file_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded and saved: {file_path}")
return file_path
else:
print(f"Failed to download {url}. Status code: {response.status_code}")
return None
def get_full_decoded_export_parquet(self, file_path: str="decoded_labels.parquet") -> str:
"""
Downloads the full decoded export of all labels in the OLI Label Pool as a Parquet file.
Args:
file_path (str): Path where the file will be saved. Defaults to "decoded_labels.parquet".
Returns:
str: Path to the downloaded Parquet file
"""
url = "https://api.growthepie.com/v1/oli/labels_decoded.parquet"
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(file_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded and saved: {file_path}")
return file_path
else:
print(f"Failed to download {url}. Status code: {response.status_code}")
return None
### OLI API ENDPOINTS ###
def post_single_attestation(self, attestation: dict) -> dict:
"""
Post a single attestation to the OLI Label Pool.
Args:
attestation (dict): The attestation object containing sig and signer
Returns:
dict: Response with uid and status (e.g., {"uid": "string", "status": "queued"})
"""
url = f"{self.api_url}/attestation"
response = requests.post(url, json=attestation)
if response.status_code == 200:
return response
else:
raise Exception(f"Failed to post single attestation: {response.status_code} - {response.text}")
def post_bulk_attestations(self, attestations: list) -> dict:
"""
Post multiple attestations to the OLI Label Pool in bulk.
Args:
attestations (list): List of attestation objects (1-1000 items)
Returns:
dict: Response with accepted count, duplicates, failed_validation, and status
"""
url = f"{self.api_url}/attestations/bulk"
if not 1 <= len(attestations) <= 1000:
raise ValueError("attestations must contain between 1 and 1000 items")
payload = {"attestations": attestations}
response = requests.post(url, json=payload)
if response.status_code == 200:
return response
else:
raise Exception(f"Failed to post bulk attestations: {response.status_code} - {response.text}")
def get_attestations(self, uid: str = None, attester: str = None, recipient: str = None, schema_info: str = None, since: str = None, order: str = "desc", limit: int = 1000) -> dict:
"""
Get raw attestations from storage with optional filters.
Args:
uid (str): Filter by specific attestation UID (0x...). If provided, other filters are ignored
attester (str): Filter by attester address (0x...)
recipient (str): Filter by recipient address (0x...)
schema_info (str): Filter by schema_info (e.g. '8453__0xabc...')
since (str): Return only attestations created after this timestamp (ISO8601 or Unix seconds)
order (str): Order results by attestation time ('asc' or 'desc'). Default: 'desc'
limit (int): Max number of attestations to return (1-1000). Default: 1000
Returns:
dict: Response with count and attestations array
"""
url = f"{self.api_url}/attestations"
params = {}
if uid:
params['uid'] = uid
if attester:
params['attester'] = attester
if recipient:
params['recipient'] = recipient
if schema_info:
params['schema_info'] = schema_info
if since:
params['since'] = since
if order:
params['order'] = order
if limit:
params['limit'] = limit
response = requests.get(url, params=params)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get attestations: {response.status_code} - {response.text}")
def post_trust_list(self, trust_list: dict) -> dict:
"""
Post a trust list to the OLI Label Pool.
Args:
trust_list (dict): The trust list object
Returns:
dict: Response with uid and status (e.g., {"uid": "string", "status": "queued"})
"""
url = f"{self.api_url}/trust-list"
response = requests.post(url, json=trust_list)
if response.status_code == 200:
return response
else:
raise Exception(f"Failed to post trust list: {response.status_code} - {response.text}")
def get_trust_lists(self, uid: str = None, attester: str = None, order: str = "desc", limit: int = 1000) -> dict:
"""
Get trust lists with optional filters.
Args:
uid (str): Filter by specific trust list UID (0x...)
attester (str): Filter by attester address (0x...)
order (str): Order by time ('asc' or 'desc'). Default: 'desc'
limit (int): Max number of rows to return (1-1000). Default: 1000
Returns:
dict: Response with count and trust_lists array
"""
url = f"{self.api_url}/trust-lists"
params = {}
if uid:
params['uid'] = uid
if attester:
params['attester'] = attester
if order:
params['order'] = order
if limit:
params['limit'] = limit
response = requests.get(url, params=params)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get trust lists: {response.status_code} - {response.text}")
def get_labels(self, address: str, chain_id: str = None, limit: int = 100, include_all: bool = False) -> dict:
"""
Get labels (key/value) for a given address. Requires API key authentication.
By default, returns only the newest label per (chain_id, attester, tag_id).
Args:
address (str): Address (0x...) [required]
chain_id (str): Optional chain_id filter (e.g., 'eip155:8453')
limit (int): Max number of labels to return (<=1000). Default: 100
include_all (bool): If False (default), return only the latest label per (chain_id, attester, tag_id)
Returns:
dict: Response with address, count, and labels array
Raises:
ValueError: If api_key is not provided
"""
if not self.api_key:
raise ValueError(
"API key is required for this endpoint. This is a protected endpoint that requires authentication.\n"
"Protected endpoints: get_labels, get_labels_bulk, search_addresses_by_tag, get_attester_analytics\n"
"Please provide an api_key parameter when calling this method.\n"
"You can obtain a free API key at https://openlabelsinitiative.org/signup"
)
url = f"{self.api_url}/labels"
headers = {'X-API-Key': self.api_key}
params = {'address': address}
if chain_id:
params['chain_id'] = chain_id
if limit:
params['limit'] = limit
if include_all is not None:
params['include_all'] = include_all
response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get labels: {response.status_code} - {response.text}")
def get_labels_bulk(self, addresses: list, chain_id: str = None, limit_per_address: int = 1000, include_all: bool = False) -> dict:
"""
Get labels for multiple addresses in bulk. Requires API key authentication.
Args:
addresses (list): List of addresses (1-100 items) [required]
chain_id (str): Optional chain_id filter (e.g., 'eip155:8453')
limit_per_address (int): Max labels to return per address (1-1000). Default: 1000
include_all (bool): Include all labels vs only latest per (chain_id, attester, tag_id). Default: False
Returns:
dict: Response with results array containing address and labels for each address
Raises:
ValueError: If api_key is not provided or if addresses list is invalid
"""
if not self.api_key:
raise ValueError(
"API key is required for this endpoint. This is a protected endpoint that requires authentication.\n"
"Protected endpoints: get_labels, get_labels_bulk, search_addresses_by_tag, get_attester_analytics\n"
"Please provide an api_key parameter when calling this method.\n"
"You can obtain a free API key at https://openlabelsinitiative.org/signup"
)
url = f"{self.api_url}/labels/bulk"
if not 1 <= len(addresses) <= 100:
raise ValueError("addresses must contain between 1 and 100 items")
headers = {'X-API-Key': self.api_key}
payload = {
"addresses": addresses,
"limit_per_address": limit_per_address,
"include_all": include_all
}
if chain_id:
payload['chain_id'] = chain_id
response = requests.post(url, json=payload, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get labels bulk: {response.status_code} - {response.text}")
def search_addresses_by_tag(self, tag_id: str, tag_value: str, chain_id: str = None, limit: int = 1000) -> dict:
"""
Search for all addresses that have a specific tag_id=tag_value pair. Requires API key authentication.
Args:
tag_id (str): The tag key (e.g., 'usage_category') [required]
tag_value (str): The tag value (e.g., 'dex') [required]
chain_id (str): Optional chain_id filter (e.g., 'eip155:8453')
limit (int): Max number of addresses to return (1-1000). Default: 1000
Returns:
dict: Response with tag_id, tag_value, count, and results array
Raises:
ValueError: If api_key is not provided
"""
if not self.api_key:
raise ValueError(
"API key is required for this endpoint. This is a protected endpoint that requires authentication.\n"
"Protected endpoints: get_labels, get_labels_bulk, search_addresses_by_tag, get_attester_analytics\n"
"Please provide an api_key parameter when calling this method.\n"
"You can obtain a free API key at https://openlabelsinitiative.org/signup"
)
url = f"{self.api_url}/addresses/search"
headers = {'X-API-Key': self.api_key}
params = {
'tag_id': tag_id,
'tag_value': tag_value
}
if chain_id:
params['chain_id'] = chain_id
if limit:
params['limit'] = limit
response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to search addresses by tag: {response.status_code} - {response.text}")
def get_attester_analytics(self, chain_id: str = None, limit: int = 100) -> dict:
"""
Get analytics summary grouped by attester. Requires API key authentication.
Returns count of labels and unique attestations per attester.
Args:
chain_id (str): Optional chain_id filter (e.g., 'eip155:8453')
limit (int): Number of rows to return (1-100). Default: 100
Returns:
dict: Response with count and results array containing attester, label_count, and unique_attestations
Raises:
ValueError: If api_key is not provided
"""
if not self.api_key:
raise ValueError(
"API key is required for this endpoint. This is a protected endpoint that requires authentication.\n"
"Protected endpoints: get_labels, get_labels_bulk, search_addresses_by_tag, get_attester_analytics\n"
"Please provide an api_key parameter when calling this method.\n"
"You can obtain a free API key at https://openlabelsinitiative.org/signup"
)
url = f"{self.api_url}/analytics/attesters"
headers = {'X-API-Key': self.api_key}
params = {}
if chain_id:
params['chain_id'] = chain_id
if limit:
params['limit'] = limit
response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get attester analytics: {response.status_code} - {response.text}")
import networkx as nx
class UtilsTrust:
def __init__(self, oli_client):
"""
Initialize the UtilsTrust with an OLI client.
Args:
oli_client: The OLI client instance
"""
self.oli = oli_client
# init trust variables
try:
self.raw_trust_lists = self.get_trust_lists_from_api()
self.TrustGraph = self.build_trust_graph(self.raw_trust_lists)
self.trust_table = self.compute_trust_table(self.TrustGraph, source_node=self.oli.source_address)
except:
self.raw_trust_lists = {}
self.trust_table = {}
self.TrustGraph = nx.MultiDiGraph()
# computes the trust table for a given source node
def compute_trust_table(self, TrustGraph: nx.MultiDiGraph, source_node: str=None) -> dict:
"""
Compute the full trust table for the source node in the trust graph.
"""
# get all tag_id & chain_id topics
tags_id = set()
chain_ids = set()
for _, _, edge_data in TrustGraph.edges(data=True):
tags_id.add(edge_data['topics']['tag_id'])
chain_ids.add(edge_data['topics']['chain_id'])
# resulting trust table
trust_table = {}
# Compute trust for all combinations
for tag_id_val in tags_id:
for chain_id_val in chain_ids:
query = {'tag_id': tag_id_val, 'chain_id': chain_id_val}
scores = self._dijkstra_trust(TrustGraph, source_node.lower(), query)
for target_node, score in scores.items():
#key = (source_node, target_node, tag_id_val, chain_id_val)
key = (target_node, tag_id_val, chain_id_val)
trust_table[key] = score
# Remove redundant entries
trust_table = self._remove_redundant_entries(trust_table)
# Keep entries ordered by highest confidence first for predictable consumption
trust_table = dict(sorted(trust_table.items(), key=lambda item: item[1], reverse=True))
return trust_table
# Build trust graph from raw trust lists
def build_trust_graph(self, raw_trust_lists) -> bool:
"""
Build the trust graph (TrustGraph) based on all trust lists stored in self.raw_trust_lists.
"""
# Keeps track of all unique nodes in the trust graph (trust list attesters + attestation attesters)
nodes = []
# Topics
edges = []
for address, tl in raw_trust_lists.items():
if address.lower() not in nodes:
nodes.append(address.lower())
for t in tl['attesters']:
if 'filters' not in t:
# Add trusted attesters (who have no filters) e.g. {'attester': '0xC139d50144Ee873c8577d682628E045dECe6040E', 'confidence': 0.8}
edges += [(address.lower(), t['address'].lower(), float(t['confidence']), {'chain_id': '*', 'tag_id': '*'})]
if t['address'].lower() not in nodes:
nodes.append(t['address'].lower())
elif 'filters' in t:
# Add trusted attesters (with filters) e.g. {'attester': '0x10dBAc4d35f4aD47E85f70c74e4449c632EC4047', 'filters': [{'tag_id': 'is_contract', 'chain_id': 'eip155:48900', 'confidence': 0.9}
for f in t['filters']:
edges += [(address.lower(), t['address'].lower(), float(f['confidence']), {'chain_id': f.get('chain_id', '*'), 'tag_id': f.get('tag_id', '*')})]
if t['address'].lower() not in nodes:
nodes.append(t['address'].lower())
# Create multi directed graph
TrustGraph = nx.MultiDiGraph()
TrustGraph.add_nodes_from(nodes)
print(f"Building trust graph based on {len(raw_trust_lists)} trust list(s), {len(nodes)} node(s) and {len(edges)} edge(s).")
# Add edges with attributes
for src, dst, trust, topics in edges:
TrustGraph.add_edge(src, dst, trust=trust, topics=topics)
return TrustGraph
def get_trust_lists_from_api(self) -> dict:
"""
Fetch trust lists (that are valid) from the OLI API and return them as a dictionary to be saved as 'oli.trust.raw_trust_lists'.
"""
trust_lists = {}
for tl in self.oli.api.get_trust_lists(order='asc')['trust_lists']:
if tl['revoked'] == False and self.oli.validate_trust_list(tl['owner_name'], tl['attesters'], tl['attestations']):
trust_lists[tl['attester']] = {
'owner_name': tl['owner_name'],
'attesters': tl['attesters'],
'attestations': tl['attestations']
}
return trust_lists
def add_trust_list(self, owner_name: str, attesters: list, attestations: list, attester_address: str) -> bool:
"""
Add (or overwrite) a trust list to the OLI trust_list variable and rebuild the trust graph.
Args:
owner_name (str): The name of the owner of the trust list
attesters (list): List of attesters in the trust list
attestations (list): List of attestations in the trust list
attester_address (str): The address of the attester providing the trust list
Returns:
bool: True if the trust list was added successfully
"""
try:
success = self.oli.validate_trust_list(owner_name, attesters, attestations)
if success:
self.raw_trust_lists[attester_address] = {
'owner_name': owner_name,
'attesters': attesters,
'attestations': attestations
}
self.TrustGraph = self.build_trust_graph(self.raw_trust_lists)
except:
raise ValueError("Trust list validation failed or 'attester_address' invalid. No trust list added to TrustGraph. See validation errors above.")
return True
# Dijkstra with no specificity logic
def _dijkstra_trust(self, G: nx.MultiDiGraph, source: str, query_topics: dict) -> dict:
trust_scores = {}
visited = set()
pq = [(-1.0, source)]
while pq:
neg_trust, node = min(pq, key=lambda x: x[0])
pq.remove((neg_trust, node))
if node in visited:
continue
visited.add(node)
current_trust = -neg_trust
for neighbor in G.successors(node):
for key, edge_data in G[node][neighbor].items():
if self._topics_match(edge_data['topics'], query_topics):
new_trust = current_trust * edge_data['trust']
if neighbor not in trust_scores or new_trust > trust_scores[neighbor]:
trust_scores[neighbor] = new_trust
pq.append((-new_trust, neighbor))
return trust_scores
# Remove entries subsumed by more general entries with >= score
def _remove_redundant_entries(self, trust_table: dict) -> dict:
cleaned = {}
by_pair = {}
for (tgt, tag, chain), score in trust_table.items():
by_pair.setdefault(tgt, []).append(((tag, chain), score))
for tgt, entries in by_pair.items():
for (tag1, chain1), score1 in entries:
is_redundant = False
for (tag2, chain2), score2 in entries:
if (tag1, chain1) == (tag2, chain2):
continue
# Is entry2 strictly more general than entry1?
tag_more_general = (tag2 == '*' < tag1 != '*')
chain_more_general = (chain2 == '*' < chain1 != '*')
if (tag_more_general or chain_more_general) and score2 >= score1:
is_redundant = True
break
if not is_redundant:
cleaned[(tgt, tag1, chain1)] = score1
return cleaned
# Do topics match function
def _topics_match(self, edge_topics: dict, query_topics: dict) -> bool:
wildcards = {'*'}
for dim, query_val in query_topics.items():
edge_val = edge_topics.get(dim)
if edge_val not in wildcards and edge_val != query_val:
return False
return True
import json
import pandas as pd
import networkx as nx
class UtilsData:
def __init__(self, oli_client):
"""
Initialize the UtilsData with an OLI client.
Args:
oli_client: The OLI client instance
"""
self.oli = oli_client
# get confidence scores
def get_confidence(self, attester: str, tag_id: str, chain_id: str) -> float:
"""
Get the confidence score for a given attester, tag_id and chain_id from the trust table.
Args:
attester (str): Attester address
tag_id (str): Tag ID
chain_id (str): Chain ID
Returns:
float: Confidence score or -1 if no score was able to be assigned
"""
# raise ValueError if trust table is empty
if self.oli.trust.trust_table == {}:
raise ValueError("Trust table is empty. Please use 'oli.set_trust_node(source_address)' function to set the source node, which will compute your trust table.")
# Checksum the attester address
attester = attester.lower()
# Iterate through self.oli.trust.trust_table in order (is sorted by confidence)
for (t_attester, t_tag, t_chain), confidence in self.oli.trust.trust_table.items():
# Check if this entry matches (with wildcard support)
if (t_attester.lower() == attester and (t_tag == tag_id or t_tag == '*') and (t_chain == chain_id or t_chain == '*')):
return confidence
return -1
+179
-56
Metadata-Version: 2.4
Name: oli-python
Version: 1.2.1
Version: 2.0.0
Summary: Python SDK for interacting with the Open Labels Initiative; A standard, registry and trust layer for EVM address labels.
Home-page: https://github.com/openlabelsinitiative/oli-python
Author: Lorenz Lehmann
Author-email: lorenz@growthepie.xyz
Author-email: lorenz@growthepie.com
Classifier: Programming Language :: Python :: 3

@@ -28,6 +28,8 @@ Classifier: License :: OSI Approved :: MIT License

# OLI Python Package
# OLI Python SDK
Python SDK for interacting with the Open Labels Initiative; A framework for address labels in the blockchain space. Read & write labels into the OLI Label Pool, check your labels for OLI compliance.
**`oli-python`** is the official Python SDK for the **[Open Labels Initiative (OLI)](https://github.com/openlabelsinitiative/OLI)**, a framework for standardized and trusted blockchain address labels.
This SDK lets you read, write, validate, and revoke address labels within the **OLI Label Pool** and compute confidence scores based on attesters' relationships within the **OLI Label Trust**.
## Installation

@@ -39,3 +41,3 @@

### Submitting New Labels
## Initialization

@@ -47,87 +49,208 @@ ```python

# Initialize the client
# Make sure to pull in your private key from an .env file
oli = OLI(private_key=os.environ['OLI_PRIVATE_KEY'], is_production=True)
oli = OLI(
private_key=os.getenv("PRIVATE_KEY"),
api_key=os.getenv("OLI_API_KEY")
)
```
# Create a label
**Setup Instructions:**
* Create a `.env` file to store your private variables
* Load an EOA wallet by setting the `private_key` variable
* The EOA wallet is used to track your reputation within OLI
* To access all advanced API endpoints, pass an `api_key` variable. You can get your free API key from **[here](http://openlabelsinitiative.org/developer)**
**Additional Options:**
You can also set the `chain` parameter to specify which EAS contracts location to use and the `custom_rpc_url` variable when initializing.
## Submit Labels into the OLI Label Pool
### Single Attestation
```python
# Define your OLI compliant attestation
address = "0x9438b8B447179740cD97869997a2FCc9b4AA63a2"
chain_id = "eip155:1" # Ethereum Mainnet
chain_id = "eip155:1" # Ethereum mainnet
tags = {
"contract_name": "growthepie donation address",
"is_eoa": True,
"owner_project": "growthepie"
"owner_project": "growthepie",
}
# Validate if your label is OLI compliant
possible_to_attest = oli.validate_label_correctness(address, chain_id, tags)
print(f"You can attest your label: {possible_to_attest}")
# (Optional) Validate your attestation before submission
is_valid = oli.validate_label(address, chain_id, tags)
print("Your attestation is OLI compliant:", is_valid)
# Submit a label as an offchain attestation
response = oli.submit_offchain_label(address, chain_id, tags)
print(f"Label submitted offchain with response: {response.text}")
# Submit label to the OLI Label Pool
response = oli.submit_label(address, chain_id, tags)
print(response)
```
# Submit a label as an onchain attestation
tx_hash, uid = oli.submit_onchain_label(address, chain_id, tags)
print(f"Label submitted onchain with hash: {tx_hash} and uid: {uid}")
### Bulk Attestation
# Batch submit multiple labels as one onchain attestation
labels = [
{'address': address, 'chain_id': chain_id, 'tags': tags},
{'address': address, 'chain_id': chain_id, 'tags': tags}
```python
attestations = [
{"address": address, "chain_id": chain_id, "tags": tags},
{"address": address, "chain_id": chain_id, "tags": tags},
]
tx_hash, uids = oli.submit_multi_onchain_labels(labels)
print(f"Labels batch submitted onchain with transaction hash: {tx_hash} and uids: {uids}")
# Revoke an attestation (revoking onchain attestations here)
trx_hash = oli.revoke_attestation(uid, onchain=True)
print(f"Label {uid} revoked with hash: {trx_hash}")
response = oli.submit_label_bulk(attestations)
print(response)
```
# Revoke multiple attestations (revoking onchain attestations here)
trx_hash, count = oli.multi_revoke_attestations(uids, onchain=True)
print(f"Labels batch revoked with hash: {trx_hash}")
## Revoke Attestations
**Note:** All revocations are onchain transactions and therefore require you to have ETH on Base for gas in your wallet.
```python
# Revoke a single attestation
oli.revoke_by_uid(uid="0x...")
# Bulk revocation
uids = ["0x1...", "0x2..."]
oli.revoke_bulk_by_uids(uids)
```
### Querying Existing Labels
## Reading the OLI Label Pool
### Get Raw Attestations
```python
from oli import OLI
import os
response = oli.get_attestations()
print(response)
```
# Initialize the client (read mode only doesn't require a private key)
oli = OLI()
**Available filters:** uid, attester, recipient, schema_info, since, order, limit
# Query attestations for a specific address
result = oli.graphql_query_attestations(address=address)
print(result)
### Get Labels for an Address
# Download parquet export of raw attestations
oli.get_full_raw_export_parquet()
```python
# All labels for an address
response = oli.get_labels(address = "0x9438b8B447179740cD97869997a2FCc9b4AA63a2")
print(response)
```
# Download parquet export of decoded attestations
oli.get_full_decoded_export_parquet()
**Available filters:** address, chain_id, limit, include_all
### Bulk Get Labels for Addresses
```python
response = ["0x9438b8B447179740cD97869997a2FCc9b4AA63a2", "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24"]
bulk = oli.get_labels_bulk(response)
print(bulk)
```
**Available filters:** addresses, chain_id, limit_per_address, include_all
## Wallet Requirements
### Get Addresses Based on a Tag
The [OLI Label Pool](https://github.com/openlabelsinitiative/OLI/tree/main/2_label_pool) lives on Base as an [Ethereum Attestation schema](https://base.easscan.org/schema/view/0xb763e62d940bed6f527dd82418e146a904e62a297b8fa765c9b3e1f0bc6fdd68).
```python
response = oli.search_addresses_by_tag(
tag_id="owner_project",
tag_value="growthepie"
)
print(response)
```
Make sure your wallet contains ETH on **Base Mainnet** to pay for onchain transaction (including offchain revocations). Offchain attestations are free.
**Available filters:** tag_id, tag_value, chain_id, limit
For testing purposes, you can use Base Sepolia Testnet by setting `is_production=False` when initializing the client.
### Attester Leaderboard
## Features
```python
chain_id = "eip155:1"
stats = oli.get_attester_analytics(chain_id)
print(stats)
```
- Submit onchain (single or batch) and offchain (single) labels into the OLI Label Pool
- Revoke your own labels (single or batch)
- Validate if your label is OLI compliant
- Query attestations using GraphQL
- Download full dataset exports in Parquet format
## OLI Label Trust
## Documentation
```python
# Set a new source node for trust propagation
trust_table = oli.set_trust_node("0xYourAddress")
print(oli.trust.trust_table)
For more details, see the [OLI Documentation](https://github.com/openlabelsinitiative/OLI).
# Import your trust list from YAML file
import yaml
with open('example_trust_list.yml', 'r') as file:
trust_list = yaml.safe_load(file)
owner_name = trust_list.get('owner_name')
attesters = trust_list.get('attesters', [])
attestations = trust_list.get('attestations', [])
# Check if the trust list is OLI compliant
is_valid = oli.validate_trust_list(
owner_name,
attesters,
attestations
)
print("Your trust list is OLI compliant:", is_valid)
# Add a private trust list to the trust graph
oli.add_trust_list(
owner_name,
attesters,
attestations
)
# Submit your trust list to the global trust graph
response = oli.submit_trust_list(
owner_name,
attesters,
attestations
)
print(response)
```
Once you set your trust node or submit your trust list, the transitive trust algorithm can assign a trust score to all labels. Use the following endpoints, which now assign a confidence score to each label. You can also set `min_confidence` to filter based on confidence thresholds.
### Get Trusted Labels for an Address
```python
# All trusted labels for an address
response = oli.get_trusted_labels(address = "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24")
print(response)
```
**Available filters:** address, chain_id, limit, include_all, min_confidence
### Bulk Get Trusted Labels for Addresses
```python
addresses = ["0x9438b8B447179740cD97869997a2FCc9b4AA63a2", "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24"]
response = oli.get_trusted_labels_bulk(addresses)
print(response)
```
**Available filters:** addresses, chain_id, limit_per_address, include_all, min_confidence
### Get Trusted Addresses Based on a Tag
```python
response = oli.search_trusted_addresses_by_tag(
tag_id = "owner_project",
tag_value = "growthepie"
)
print(response)
```
**Available filters:** tag_id, tag_value, chain_id, limit, min_confidence
## OLI Label Pool Parquet Exports
growthepie provides dataset exports as Parquet files for all attestations inside the OLI Label Pool, updated daily:
```python
oli.get_full_raw_export_parquet()
oli.get_full_decoded_export_parquet()
```
**Option:** Pass the filepath as a variable to store the export in a specific location.
## OLI Documentation
* [Open Labels Initiative Docs](https://github.com/openlabelsinitiative/OLI)
* [OLI Website](https://www.openlabelsinitiative.org/)
## License
MIT
MIT © Open Labels Initiative

@@ -11,4 +11,5 @@ README.md

oli/data/__init__.py
oli/data/fetcher.py
oli/data/graphql.py
oli/data/api.py
oli/data/trust.py
oli/data/utils.py
oli_python.egg-info/PKG-INFO

@@ -15,0 +16,0 @@ oli_python.egg-info/SOURCES.txt

from oli.core import OLI
__version__ = "0.1.0"
__all__ = ["OLI"]

@@ -8,3 +8,3 @@ import time

class OffchainAttestations:
def __init__(self, oli_client):
def __init__(self, oli_client, private_key: str):
"""

@@ -17,2 +17,3 @@ Initialize OffchainAttestations with an OLI client.

self.oli = oli_client
self._private_key = private_key

@@ -33,8 +34,14 @@ def submit_offchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", retry: int=4):

"""
# fix simple formatting errors in tags
tags = self.oli.validator.fix_simple_tags_formatting(tags)
# in case tag definition fails to load, skip tag formatting and validation
if self.oli.tag_definitions is not None:
# Check all necessary input parameters
self.oli.validator.validate_label_correctness(address, chain_id, tags, ref_uid, auto_fix=False)
# fix simple formatting errors in tags
tags = self.oli.validator.fix_simple_tags_formatting(tags)
# Check all necessary input parameters
self.oli.validator.validate_label_correctness(address, chain_id, tags, ref_uid, auto_fix=False)
else:
print("Warning: OLI tag definitions not loaded, skipping tag formatting and validation. Please upgrade to the latest OLI version and ensure internet connectivity at initialization.")
# Encode the label data

@@ -50,5 +57,13 @@ data = self.oli.utils_other.encode_label_data(chain_id, tags)

)
# Convert numerical values to strings for JSON serialization
attestation["sig"]["message"]["time"] = str(attestation["sig"]["message"]["time"])
attestation["sig"]["message"]["expirationTime"] = str(attestation["sig"]["message"]["expirationTime"])
attestation["sig"]["domain"]["chainId"] = str(attestation["sig"]["domain"]["chainId"])
# extract uid
uid = attestation["sig"]["uid"]
# Post to the API & retry if status code is not 200
response = self.post_offchain_attestation(attestation)
response = self.oli.api.post_single_attestation(attestation)
n0 = retry

@@ -58,28 +73,109 @@ while response.status_code != 200 and retry > 0:

time.sleep(2 ** (n0 - retry)) # exponential backoff
# rebuild the attestation (assigns new timestamp) to not get rate limited by EAS post endpoint
attestation = self.build_offchain_attestation(
recipient=address,
schema=self.oli.oli_label_pool_schema,
data=data,
ref_uid=ref_uid
)
response = self.post_offchain_attestation(attestation)
response = self.oli.api.post_single_attestation(attestation)
# if it fails after all retries, raise an error
if response.status_code != 200:
raise Exception(f"Failed to submit offchain attestation to EAS API ipfs post endpoint after {n0} retries: {response.status_code} - {response.text}")
raise Exception(f"Failed to submit offchain attestation label to OLI API post endpoint 'attestation' after {n0} retries: {response.status_code} - {response.text}")
return response
return response, uid
def post_offchain_attestation(self, attestation: dict, filename: str="OLI.txt") -> Response:
def submit_offchain_labels_bulk(self, attestations: list, retry: int=4) -> dict:
"""
Post API an attestation to the EAS API.
Submit up to 1000 OLI labels as offchain attestations to the OLI Label Pool in bulk.
Args:
attestation (dict): The attestation package
filename (str): Custom filename
attestations (list): List of attestation objects (1-1000 items), each containing address, chain_id, tags (and optional ref_uid)
retry (int): Number of retries for the API post request to EAS ipfs
Returns:
dict: API response
dict: API request response
"""
signed_attestations = []
uids = []
for attestation in attestations:
address = attestation.get("address")
chain_id = attestation.get("chain_id")
tags = attestation.get("tags", None) # no tags means no attestation will be created
ref_uid = attestation.get("ref_uid", "0x0000000000000000000000000000000000000000000000000000000000000000")
if tags is not None:
# in case tag definition fails to load, skip tag formatting and validation
if self.oli.tag_definitions is not None:
# fix simple formatting errors in tags
tags = self.oli.validator.fix_simple_tags_formatting(tags)
# Check all necessary input parameters
self.oli.validator.validate_label_correctness(address, chain_id, tags, ref_uid, auto_fix=False)
else:
print("Warning: OLI tag definitions not loaded, skipping tag formatting and validation. Please upgrade to the latest OLI version and ensure internet connectivity at initialization.")
# Encode the label data
data = self.oli.utils_other.encode_label_data(chain_id, tags)
# Build the attestation
attestation = self.build_offchain_attestation(
recipient=address,
schema=self.oli.oli_label_pool_schema,
data=data,
ref_uid=ref_uid
)
# Convert numerical values to strings for JSON serialization
attestation["sig"]["message"]["time"] = str(attestation["sig"]["message"]["time"])
attestation["sig"]["message"]["expirationTime"] = str(attestation["sig"]["message"]["expirationTime"])
attestation["sig"]["domain"]["chainId"] = str(attestation["sig"]["domain"]["chainId"])
# extract uid
uid = attestation["sig"]["uid"]
uids.append(uid)
signed_attestations.append(attestation)
# Post to the API & retry if status code is not 200
response = self.oli.api.post_bulk_attestations(signed_attestations)
n0 = retry
while response.status_code != 200 and retry > 0:
retry -= 1
time.sleep(2 ** (n0 - retry)) # exponential backoff
response = self.oli.api.post_bulk_attestations(signed_attestations)
# if it fails after all retries, raise an error
if response.status_code != 200:
raise Exception(f"Failed to submit offchain attestation labels bulk to OLI API post endpoint 'attestation/bulk' after {n0} retries: {response.status_code} - {response.text}")
return response, uids
def submit_offchain_trust_list(self, owner_name: str='', attesters: list=[], attestations: list=[], retry: int=4) -> dict:
"""
Submit an OLI trust list as an offchain attestation to the OLI Trust List Pool.
Args:
owner (str): The address of the owner of the trust list
attesters (list): List of dictionaries containing attester information with trust scores and optional filters
attestations (list): List of dictionaries containing attestation information with trust scores
retry (int): Number of retries for the API post request to EAS ipfs
Returns:
dict: API request response
"""
# Validate the trust list
self.oli.validator.validate_trust_list_correctness(owner_name, attesters, attestations)
# Encode the label data
data = self.oli.utils_other.encode_list_data(owner_name, attesters, attestations)
# Build the attestation
recipient = "0x0000000000000000000000000000000000000000"
ref_uid = "0x0000000000000000000000000000000000000000000000000000000000000000"
attestation = self.build_offchain_attestation(
recipient=recipient,
schema=self.oli.oli_label_trust_schema,
data=data,
ref_uid=ref_uid
)
# Convert numerical values to strings for JSON serialization

@@ -89,16 +185,19 @@ attestation["sig"]["message"]["time"] = str(attestation["sig"]["message"]["time"])

attestation["sig"]["domain"]["chainId"] = str(attestation["sig"]["domain"]["chainId"])
# extract uid
uid = attestation["sig"]["uid"]
# Post to the API & retry if status code is not 200
response = self.oli.api.post_trust_list(attestation)
n0 = retry
while response.status_code != 200 and retry > 0:
retry -= 1
time.sleep(2 ** (n0 - retry)) # exponential backoff
response = self.oli.api.post_trust_list(attestation)
# Prepare payload for the API endpoint
payload = {
"filename": filename,
"textJson": json.dumps(attestation, separators=(',', ':'))
}
headers = {
"Content-Type": "application/json"
}
# Post the data to the API
response = requests.post(self.oli.eas_api_url, json=payload, headers=headers)
return response
# if it fails after all retries, raise an error
if response.status_code != 200:
raise Exception(f"Failed to submit offchain attestation trust list to OLI API post endpoint 'trust-list' after {n0} retries: {response.status_code} - {response.text}")
return response, uid

@@ -226,3 +325,3 @@ def build_offchain_attestation(self, recipient: str, schema: str, data: str, ref_uid: str, revocable: bool=True, expiration_time: int=0) -> dict:

# Sign the transaction
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self._private_key)

@@ -275,3 +374,3 @@ # Send the transaction

# Sign the transaction
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self._private_key)

@@ -286,4 +385,4 @@ # Send the transaction

if txn_receipt.status == 1:
return f"0x{txn_hash.hex()}", len(uids)
return f"0x{txn_hash.hex()}"
else:
raise Exception(f"Transaction failed: {txn_receipt}")
class OnchainAttestations:
def __init__(self, oli_client):
def __init__(self, oli_client, private_key: str):
"""

@@ -10,4 +10,5 @@ Initialize OnchainAttestations with an OLI client.

self.oli = oli_client
self.__private_key = private_key
def submit_onchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", gas_limit: int=0) -> tuple[str, str]:
def submit_onchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", gas_limit: int=-1) -> tuple[str, str]:
"""

@@ -21,3 +22,3 @@ Submit an OLI label as an onchain attestation to the OLI Label Pool.

ref_uid (str): Reference UID
gas_limit (int): Gas limit for the transaction. If set to 0, the function will estimate the gas limit.
gas_limit (int): Gas limit for the transaction. If set to -1, the function will estimate the gas limit.

@@ -28,8 +29,14 @@ Returns:

"""
# fix simple formatting errors in tags
tags = self.oli.validator.fix_simple_tags_formatting(tags)
# in case tag definition fails to load, skip tag formatting and validation
if self.oli.tag_definitions is not None:
# Check all necessary input parameters
self.oli.validator.validate_label_correctness(address, chain_id, tags, ref_uid, auto_fix=False)
# fix simple formatting errors in tags
tags = self.oli.validator.fix_simple_tags_formatting(tags)
# Check all necessary input parameters
self.oli.validator.validate_label_correctness(address, chain_id, tags, ref_uid, auto_fix=False)
else:
print("Warning: OLI tag definitions not loaded, skipping tag formatting and validation. Please upgrade to the latest OLI version and ensure internet connectivity at initialization.")
# Encode the label data

@@ -65,3 +72,3 @@ data = self.oli.utils_other.encode_label_data(chain_id, tags)

# Sign the transaction with the private key
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.__private_key)

@@ -82,4 +89,4 @@ # Send the transaction

raise Exception(f"Transaction failed onchain: {txn_receipt}")
def submit_multi_onchain_labels(self, labels: list, gas_limit: int=0) -> tuple[str, list]:
def submit_multi_onchain_labels(self, labels: list, gas_limit: int=-1) -> tuple[str, list]:
"""

@@ -94,3 +101,3 @@ Batch submit multiple OLI labels as an onchain attestation to the OLI Label Pool.

ref_uid (str): Reference UID
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.
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 -1, the function will estimate the gas limit.

@@ -112,8 +119,14 @@ Returns:

# fix simple formatting errors in tags
label['tags'] = self.oli.validator.fix_simple_tags_formatting(label['tags'])
# in case tag definition fails to load, skip tag formatting and validation
if self.oli.tag_definitions is not None:
# fix simple formatting errors in tags
label['tags'] = self.oli.validator.fix_simple_tags_formatting(label['tags'])
# run checks on each label
self.oli.validator.validate_label_correctness(label['address'], label['chain_id'], label['tags'], auto_fix=False)
# run checks on each label
self.oli.validator.validate_label_correctness(label['address'], label['chain_id'], label['tags'], auto_fix=False)
else:
print("Warning: OLI tag definitions not loaded, skipping tag formatting and validation. Please upgrade to the latest OLI version and ensure internet connectivity at initialization.")
# check if ref_uid is provided

@@ -159,3 +172,3 @@ if 'ref_uid' not in label:

# Sign the transaction with the private key
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.__private_key)

@@ -179,3 +192,67 @@ # Send the transaction

return f"0x{txn_hash.hex()}", uids
def submit_onchain_trust_list(self, owner: str, trusted: list, untrusted: list, gas_limit: int=-1) -> tuple[str, str]:
"""
Submit an OLI trust list as an onchain attestation to the OLI Trust List Pool.
Args:
owner (str): The address of the owner of the trust list
trusted (list): List of dictionaries containing trusted addresses
untrusted (list): List of dictionaries containing untrusted addresses
gas_limit (int): Gas limit for the transaction. If set to -1, the function will estimate the gas limit.
Returns:
str: Transaction hash
str: UID of the attestation
"""
# Validate the trust list
self.oli.validator.validate_trust_list_correctness(owner, trusted, untrusted)
# Encode the label data
data = self.oli.utils_other.encode_list_data(owner, trusted, untrusted)
# Create the attestation
function = self.oli.eas.functions.attest({
'schema': self.oli.w3.to_bytes(hexstr=self.oli.oli_label_trust_schema),
'data': {
'recipient': "0x0000000000000000000000000000000000000000", # Trust lists are not tied to a specific address
'expirationTime': 0, # never expires
'revocable': True, # can be revoked
'refUID': "0x0000000000000000000000000000000000000000000000000000000000000000", # no ref UID for trust lists
'data': self.oli.w3.to_bytes(hexstr=data),
'value': 0
}
})
# Define the transaction parameters
tx_params = {
'chainId': self.oli.rpc_chain_number,
'gasPrice': self.oli.w3.eth.gas_price,
'nonce': self.oli.w3.eth.get_transaction_count(self.oli.address),
}
# Estimate gas if no limit provided
tx_params = self.oli.utils_other.estimate_gas_limit(function, tx_params, gas_limit)
# Build the transaction to attest one label
transaction = function.build_transaction(tx_params)
# Sign the transaction with the private key
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.__private_key)
# Send the transaction
try:
txn_hash = self.oli.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
except Exception as e:
raise Exception(f"Failed to send transaction to mempool: {e}")
# Wait for the transaction receipt
txn_receipt = self.oli.w3.eth.wait_for_transaction_receipt(txn_hash)
# Check if the transaction was successful
if txn_receipt.status == 1:
return f"0x{txn_hash.hex()}", f"0x{txn_receipt.logs[0].data.hex()}"
else:
raise Exception(f"Transaction failed onchain: {txn_receipt}")
def revoke_attestation(self, uid_hex: str, gas_limit: int=200000) -> str:

@@ -214,3 +291,3 @@ """

# Sign the transaction
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.__private_key)

@@ -270,3 +347,3 @@ # Send the transaction

# Sign the transaction
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.oli.private_key)
signed_txn = self.oli.w3.eth.account.sign_transaction(transaction, private_key=self.__private_key)

@@ -281,4 +358,4 @@ # Send the transaction

if txn_receipt.status == 1:
return f"0x{txn_hash.hex()}", len(uids)
return f"0x{txn_hash.hex()}"
else:
raise Exception(f"Transaction failed: {txn_receipt}")

@@ -35,2 +35,23 @@ import json

def encode_list_data(self, owner: str, trusted: list, untrusted: list) -> str:
"""
Encode trust list data in the OLI format.
Args:
owner (str): Owner name of the trust list
trusted (dict): Dictionary of trusted entities
untrusted (dict): Dictionary of untrusted entities
Returns:
str: Hex-encoded ABI data
"""
# Convert list to JSON strings if needed
if isinstance(trusted, list):
trusted = json.dumps(trusted)
if isinstance(untrusted, list):
untrusted = json.dumps(untrusted)
# ABI encode the data
encoded_data = encode(['string', 'string', 'string'], [owner, trusted, untrusted])
return f"0x{encoded_data.hex()}"
def estimate_gas_limit(self, function, tx_params: dict, gas_limit: int) -> dict:

@@ -49,3 +70,3 @@ """

try:
if gas_limit == 0:
if gas_limit == -1:
# Estimate gas with a buffer (e.g., 10% more than the estimate)

@@ -52,0 +73,0 @@ estimated_gas = function.estimate_gas(tx_params)

@@ -18,2 +18,8 @@ class UtilsValidator:

]
# URLs for helpful resources
self.url_1_label_schema = "https://github.com/openlabelsinitiative/OLI/tree/main/1_label_schema"
self.url_2_label_pool = "https://github.com/openlabelsinitiative/OLI/tree/main/2_label_pool"
self.url_3_label_trust = "https://github.com/openlabelsinitiative/OLI/tree/main/3_label_trust"
self.url_tag_definitions = "https://github.com/openlabelsinitiative/OLI/blob/main/1_label_schema/tags/tag_definitions.yml"
self.url_caip2_format = "https://docs.portalhq.io/resources/chain-id-formatting"

@@ -58,3 +64,3 @@ def fix_simple_tags_formatting(self, tags: dict) -> dict:

"""
Validates if the label is compliant with the OLI Data Model. See OLI Github documentation for more details: https://github.com/openlabelsinitiative/OLI
Validates if the label is compliant with the OLI Label Schema. See OLI Github documentation for more details: https://github.com/openlabelsinitiative/OLI/tree/main/1_label_schema

@@ -77,3 +83,25 @@ Args:

return True
def validate_trust_list_correctness(self, owner_name: str, attesters: list, attestations: list) -> bool:
"""
Validates if the trust list is compliant with the OLI Label Trust definitions. See OLI Github documentation for more details: https://github.com/openlabelsinitiative/OLI/tree/main/3_label_trust
Args:
owner (str): Owner name of the trust list
attesters (list): List of attester information with confidence scores and optional filters
attestations (list): List of attestation information with confidence scores
Returns:
bool: True if the trust list is correct, False otherwise
"""
if owner_name.isascii() != True:
print(owner_name)
raise ValueError("Owner name must only contain ASCII characters")
elif len(owner_name) < 3 or len(owner_name) > 100:
print(owner_name)
raise ValueError("Owner name must be between 3 and 100 characters long")
self.validate_attesters_list(attesters)
self.validate_attestations_list(attestations)
return True
def validate_chain_id(self, chain_id: str) -> bool:

@@ -107,3 +135,3 @@ """

print(f"Unsupported chain ID format: {chain_id}")
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")
raise ValueError(f"Chain ID must be in CAIP-2 format (e.g., Base -> 'eip155:8453'), see this guide on CAIP-2: {self.url_caip2_format}")

@@ -144,3 +172,3 @@ def validate_address(self, address: str) -> bool:

print(tags)
raise ValueError("Tags must be a dictionary with OLI compliant tags (e.g., {'contract_name': 'example', 'is_eoa': True})")
raise ValueError(f"Tags must be a dictionary with OLI compliant tags (e.g., {{'contract_name': 'example', 'is_eoa': True}}). See for example: {self.url_1_label_schema}")

@@ -152,3 +180,3 @@ # Check each tag_id in the dictionary # TODO: redo this with tag_definitions 2.0 and schema, should be more efficient

if tag_id not in self.oli.tag_ids:
print(f"WARNING: Tag tag_id '{tag_id}' is not an official OLI tag. Please check 'oli.tag_definitions' or https://github.com/openlabelsinitiative/OLI/blob/main/1_label_schema/tags/tag_definitions.yml.")
print(f"WARNING: Tag tag_id '{tag_id}' is not an official OLI tag. Please check 'oli.tag_definitions' or {self.url_tag_definitions}.")

@@ -221,2 +249,112 @@ # 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)]

print(ref_uid)
raise ValueError("Ref_uid must be a valid UID in hex format, leave empty if not used")
raise ValueError("Ref_uid must be a valid UID in hex format")
def validate_attesters_list(self, attesters: list) -> bool:
"""
Validates if the attester list is in the correct format.
Args:
attesters (list): Attester list to check
Returns:
bool: True if correct, throws error otherwise
"""
if not isinstance(attesters, list):
raise ValueError("Attesters list must be a list.")
for item in attesters:
# Validate attester
if 'address' in item:
# check attester address
self.validate_address(item['address'])
# Check attester item
self.validate_attester_item(item)
else:
print(item)
raise ValueError(f"Each attester entry must have an 'address' key. See for example: {self.url_3_label_trust}")
return True
def validate_attestations_list(self, attestations: list) -> bool:
"""
Validates if the attestations list is in the correct format.
Args:
untrusted (list): Untrusted list to check
Returns:
bool: True if correct, throws error otherwise
"""
if not isinstance(attestations, list):
raise ValueError("Attestations list must be a list.")
for item in attestations:
# Validate attestation
if 'uid' in item:
# check attestation UID
self.validate_ref_uid(item['uid'])
# check attestation item
self.validate_attestation_item(item)
else:
print(item)
raise ValueError(f"Each attestation entry must have an 'uid' key. See for example: {self.url_3_label_trust}")
return True
def validate_attester_item(self, item: dict) -> bool:
"""
Validates if an attester item is in the correct format.
Args:
item (dict): Attester item to check
Returns:
bool: True if valid, throws error otherwise
"""
# Check if confidence key is present
if 'confidence' in item:
if 'filters' in item:
# if 'confidence' is assigned to an attester, then specific confidence scores based on 'filters' are not allowed
print(item)
raise ValueError(f"If a 'confidence' key is assigned to an attester, specific confidence scores based on 'filters' are not allowed. Please remove 'filters'. See for example: {self.url_3_label_trust}")
elif item['confidence'] < 0 or item['confidence'] > 1:
print(item)
raise ValueError("'confidence' scores must be between 0 and 1")
elif 'filters' in item:
# make sure tag_id or chain_id is present in each entry in filters
if isinstance(item['filters'], list):
for tag in item['filters']:
if 'tag_id' not in tag and 'chain_id' not in tag:
print(item['filters'])
raise ValueError(f"Each filter must have a at least a key for 'tag_id' or 'chain_id'. See for example: {self.url_3_label_trust}")
elif 'confidence' not in tag:
print(item['filters'])
raise ValueError(f"Each filter must have a key called 'confidence'. See for example: {self.url_3_label_trust}")
elif tag['confidence'] < 0 or tag['confidence'] > 1:
print(item['filters'])
raise ValueError("Each 'confidence' score must be between 0 and 1")
else:
print(item['filters'])
raise ValueError(f"'filters' must be a list of dictionaries with 'tag_id' or 'chain_id' filters and a 'confidence' score between 0 and 1. See for example: {self.url_3_label_trust}")
else:
print(item)
raise ValueError(f"Each attester entry must have either have a 'confidence' or 'filters' key. See for example: {self.url_3_label_trust}")
return True
def validate_attestation_item(self, item: dict) -> bool:
"""
Validates if an attestation item is in the correct format.
Args:
item (dict): Attestation item to check
Returns:
bool: True if valid, throws error otherwise
"""
if 'confidence' not in item:
print(item)
raise ValueError(f"Each attestation entry with a 'uid' key must also have a 'confidence' key. See for example: {self.url_3_label_trust}")
elif item['confidence'] < 0 or item['confidence'] > 1:
print(item)
raise ValueError(f"Confidence must be between 0 and 1.")
return True
from web3 import Web3
import eth_account
import pandas as pd
import os

@@ -11,7 +12,27 @@ from eth_keys import keys

from oli.attestation.offchain import OffchainAttestations
from oli.data.fetcher import DataFetcher
from oli.data.graphql import GraphQLClient
from oli.data.api import API
from oli.data.utils import UtilsData
from oli.data.trust import UtilsTrust
from dataclasses import dataclass
@dataclass
class PostResponse:
"""Standardized response for all POST operations"""
success: bool
onchain: bool
transaction_hash: str = None
uid: str = None
uids: list = None
eas_schema_chain: str = None # only for onchain operations
eas_schema: str = None
status: str = None
accepted: int = None # for bulk operations
duplicates: int = None # for bulk operations
failed_validation: list = None # for bulk operations
error: str = None
def to_dict(self) -> dict:
return {k: v for k, v in self.__dict__.items() if v is not None}
class OLI:
def __init__(self, private_key: str=None, is_production: bool=True, custom_rpc_url: str=None) -> None:
def __init__(self, private_key: str=None, chain: str='Base', api_key: str=None, custom_rpc_url: str=None) -> None:
"""

@@ -22,3 +43,3 @@ Initialize the OLI API client.

private_key (str): The private key to sign attestations
is_production (bool): Whether to use production or testnet
chain (string): The blockchain network to connect to (e.g., 'Base', 'Arbitrum')
custom_rpc_url (str): Custom RPC URL to connect to Blockchain

@@ -28,113 +49,313 @@ """

# Set network based on environment
if is_production:
# Set network (EAS contracts need to be deployed on the target chain)
if chain.lower() == 'base':
self.rpc = "https://mainnet.base.org"
self.graphql = "https://base.easscan.org/graphql"
self.rpc_chain_number = 8453
self.eas_api_url = "https://base.easscan.org/offchain/store"
self.eas_address = "0x4200000000000000000000000000000000000021" # EAS contract address on mainnet
else:
self.rpc = "https://sepolia.base.org"
self.graphql = "https://base-sepolia.easscan.org/graphql"
self.rpc_chain_number = 84532
self.eas_api_url = "https://base-sepolia.easscan.org/offchain/store"
self.eas_address = "0x4200000000000000000000000000000000000021" # EAS contract address on testnet
raise ValueError(f"Unsupported chain: {chain}. Currently only 'Base' is supported.")
# Use provided RPC endpoint if specified
# Use provided RPC endpoint if nothing else specified
if custom_rpc_url is not None:
self.rpc = custom_rpc_url
# Initialize Web3 and account
# Initialize Web3 instance from RPC endpoint
self.w3 = Web3(Web3.HTTPProvider(self.rpc))
if not self.w3.is_connected():
raise Exception("Failed to connect to the Ethereum node")
# Try to get private key from environment if not provided
# Set local account using private key
if private_key is None:
private_key = os.environ.get('OLI_PRIVATE_KEY')
if not private_key:
print("WARNING: Private key not provided. Please set the OLI_PRIVATE_KEY environment variable in case you plan to submit labels.")
print("WARNING: OLI client in read mode only.")
print("WARNING: Private key not provided. Please set 'private_key' variable when initializing OLI.")
print("WARNING: OLI client in read mode only.")
else:
try:
# Convert the hex private key to the proper key object
if private_key.startswith('0x'):
private_key_bytes = private_key[2:]
else:
private_key_bytes = private_key
# Create account from private key
private_key_obj = keys.PrivateKey(bytes.fromhex(private_key_bytes))
self.account = eth_account.Account.from_key(private_key_obj)
self.address = self.account.address
self.source_address = self.address # for Label Trust
except Exception as e:
print(f"Error initializing account from private key: {e}")
self.account = None
self.address = None
self.source_address = None # for Label Trust
self.private_key = private_key
if self.private_key:
# Convert the hex private key to the proper key object
if private_key.startswith('0x'):
private_key_bytes = private_key[2:]
else:
private_key_bytes = private_key
# Create account from private key
private_key_obj = keys.PrivateKey(bytes.fromhex(private_key_bytes))
self.account = eth_account.Account.from_key(private_key_obj)
self.address = self.account.address
# Label Pool Schema for OLI
# Label Pool Schema for OLI Label Pool
self.oli_label_pool_schema = '0xb763e62d940bed6f527dd82418e146a904e62a297b8fa765c9b3e1f0bc6fdd68'
# Load EAS ABI
# Label Trust Schema for OLI Label Trust
self.oli_label_trust_schema = '0x6d780a85bfad501090cd82868a0c773c09beafda609d54888a65c106898c363d'
# Load EAS ABI & contract
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"}]'
self.eas = self.w3.eth.contract(address=self.eas_address, abi=self.eas_abi)
# Initialize EAS contract
self.eas = self.w3.eth.contract(address=self.eas_address, abi=self.eas_abi)
# Initialize components
self.data_fetcher = DataFetcher(self)
self.tag_definitions = self.data_fetcher.get_OLI_tags()
self.tag_ids = list(self.tag_definitions.keys())
self.tag_value_sets = self.data_fetcher.get_OLI_value_sets()
# Initialize validator
# Initialize OLI Label Schema
self.api = API(self, api_key=api_key)
self.tag_definitions = self.api.get_OLI_tags()
try:
self.tag_ids = list(self.tag_definitions.keys())
self.tag_value_sets = self.api.get_OLI_value_sets()
except Exception as e:
self.tag_ids = []
self.tag_value_sets = {}
# Initialize OLI Label Pool
self.onchain = OnchainAttestations(self, private_key)
self.offchain = OffchainAttestations(self, private_key)
self.utils_other = UtilsOther(self)
self.validator = UtilsValidator(self)
# Initialize other utilities
self.utils_other = UtilsOther(self)
# Initialize onchain and offchain attestations
self.onchain = OnchainAttestations(self)
self.offchain = OffchainAttestations(self)
# Initialize GraphQL client
self.graphql_client = GraphQLClient(self)
# Initialize OLI Label Trust
self.utils_data = UtilsData(self)
self.trust = UtilsTrust(self)
print("...OLI client successfully initialized.")
# OLI Label Pool post functions
def submit_label(self, address: str, chain_id: str, tags: dict, onchain: bool=False, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", gas_limit: int=-1, retry: int=4) -> PostResponse:
try:
if onchain:
txn_hash, uid = self.onchain.submit_onchain_label(address, chain_id, tags, ref_uid, gas_limit)
return PostResponse(
success=True,
onchain=True,
transaction_hash=txn_hash,
uid=uid,
eas_schema_chain=self.rpc_chain_number,
eas_schema=self.oli_label_pool_schema
).to_dict()
else:
response, uid = self.offchain.submit_offchain_label(address, chain_id, tags, ref_uid, retry)
data = response.json()
return PostResponse(
success=True,
onchain=False,
uid=uid if isinstance(uid, str) else uid[0] if uid else None,
eas_schema=self.oli_label_pool_schema,
status=data.get('status')
).to_dict()
except Exception as e:
return PostResponse(
success=False,
onchain=onchain,
error=str(e)
).to_dict()
def submit_label_bulk(self, labels: list, onchain: bool=False, gas_limit: int=-1, retry: int=4) -> PostResponse:
try:
if onchain:
txn_hash, uids = self.onchain.submit_multi_onchain_labels(labels, gas_limit)
return PostResponse(
success=True,
onchain=True,
transaction_hash=txn_hash,
uids=uids if isinstance(uids, list) else [uids],
eas_schema_chain=self.rpc_chain_number,
eas_schema=self.oli_label_pool_schema
).to_dict()
else:
response, uids = self.offchain.submit_offchain_labels_bulk(labels, retry)
data = response.json()
return PostResponse(
success=True,
onchain=False,
uids=uids if uids else None,
eas_schema=self.oli_label_pool_schema,
status=data.get('status'),
accepted=data.get('accepted'),
duplicates=data.get('duplicates'),
failed_validation=data.get('failed_validation')
).to_dict()
except Exception as e:
return PostResponse(
success=False,
onchain=onchain,
error=str(e)
).to_dict()
def submit_trust_list(self, owner_name: str, attesters: list=[], attestations: list=[], onchain: bool=False, gas_limit: int=-1, retry: int=4) -> PostResponse:
try:
if onchain:
txn_hash, uid = self.onchain.submit_onchain_trust_list(owner_name, attesters, attestations, gas_limit)
return PostResponse(
success=True,
onchain=True,
transaction_hash=txn_hash,
uid=uid,
eas_schema_chain=self.rpc_chain_number,
eas_schema=self.oli_label_trust_schema
).to_dict()
else:
response, uid = self.offchain.submit_offchain_trust_list(owner_name, attesters, attestations, retry)
data = response.json()
return PostResponse(
success=True,
onchain=False,
uid=uid if isinstance(uid, str) else uid[0] if uid else None,
eas_schema=self.oli_label_trust_schema,
status=data.get('status')
).to_dict()
except Exception as e:
return PostResponse(
success=False,
onchain=onchain,
error=str(e)
).to_dict()
# Expose onchain attestation methods
def submit_onchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", gas_limit: int=0) -> tuple[str, str]:
return self.onchain.submit_onchain_label(address, chain_id, tags, ref_uid, gas_limit)
def submit_multi_onchain_labels(self, labels: list, gas_limit: int=0) -> tuple[str, list]:
return self.onchain.submit_multi_onchain_labels(labels, gas_limit)
# Expose offchain attestation methods
def submit_offchain_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", retry: int=4) -> Response:
return self.offchain.submit_offchain_label(address, chain_id, tags, ref_uid, retry)
# Expose revocation methods
def revoke_attestation(self, uid_hex: str, onchain: bool, gas_limit: int=200000) -> str:
if onchain:
return self.onchain.revoke_attestation(uid_hex, gas_limit)
else:
return self.offchain.revoke_attestation(uid_hex, gas_limit)
def multi_revoke_attestations(self, uids: str, onchain: bool, gas_limit: int=10000000) -> str:
if onchain:
return self.onchain.multi_revoke_attestations(uids, gas_limit)
else:
return self.offchain.multi_revoke_attestations(uids, gas_limit)
# Expose query methods
def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None, take: int=None, id: str=None,expand_json: bool=True) -> dict:
return self.graphql_client.graphql_query_attestations(address, attester, timeCreated, revocationTime, take, id, expand_json)
def revoke_by_uid(self, uid: str, onchain: bool=False, gas_limit: int=200000) -> dict:
# please note onchain revocations require ETH for gas fees
try:
# revoke based on onchain parameter
if onchain:
txn_hash = self.onchain.revoke_attestation(uid, gas_limit)
else:
txn_hash = self.offchain.revoke_attestation(uid, gas_limit)
return PostResponse(
success=True,
onchain=True,
transaction_hash=txn_hash,
uid=uid,
eas_schema_chain=self.rpc_chain_number
).to_dict()
except Exception as e:
return PostResponse(
success=False,
onchain=True,
uid=uid,
eas_schema_chain=self.rpc_chain_number,
error=str(e)
).to_dict()
def revoke_bulk_by_uids(self, uids: list, onchain: bool=False, gas_limit: int=10000000) -> dict:
# please note onchain revocations require ETH for gas fees, furthermore it is recommended to keep the max number of uids to 100
try:
# revoke based on onchain parameter
if onchain:
txn_hash = self.onchain.multi_revoke_attestations(uids, gas_limit)
else:
txn_hash = self.offchain.multi_revoke_attestations(uids, gas_limit)
return PostResponse(
success=True,
onchain=True,
transaction_hash=txn_hash,
uids=uids,
eas_schema_chain=self.rpc_chain_number
).to_dict()
except Exception as e:
return PostResponse(
success=False,
onchain=True,
uids=uids,
eas_schema_chain=self.rpc_chain_number,
error=str(e)
).to_dict()
# OLI Label Pool validation functions
def validate_label(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", auto_fix: bool=True) -> bool:
return self.validator.validate_label_correctness(address, chain_id, tags, ref_uid, auto_fix)
def validate_trust_list(self, owner_name: str, attesters: list=[], attestations: list=[]) -> bool:
return self.validator.validate_trust_list_correctness(owner_name, attesters, attestations)
# OLI Label Pool parquet exports
def get_full_raw_export_parquet(self, file_path: str="raw_labels.parquet") -> str:
return self.data_fetcher.get_full_raw_export_parquet(file_path)
return self.api.get_full_raw_export_parquet(file_path)
def get_full_decoded_export_parquet(self, file_path: str="decoded_labels.parquet") -> str:
return self.data_fetcher.get_full_decoded_export_parquet(file_path)
return self.api.get_full_decoded_export_parquet(file_path)
# OLI Label Trust functions
def set_trust_node(self, address: str) -> None:
if self.validator.validate_address(address):
# set the source address for trust calculations
self.source_address = address
# compute the trust table for the new source address
self.trust.trust_table = self.trust.compute_trust_table(self.trust.TrustGraph, source_node=address)
return self.trust.trust_table
def add_trust_list(self, owner_name: str='private_node', attesters: list=[], attestations: list=[], attester_address: str='0x0000000000000000000000000000000000000000') -> None:
# add the trust list to the trust graph
success = self.trust.add_trust_list(owner_name, attesters, attestations, attester_address)
if success:
# recompute the trust table for the current saved 'source_address'
self.trust.trust_table = self.trust.compute_trust_table(self.trust.TrustGraph, source_node=self.source_address)
return True
# OLI Label Pool API functions (no api key required)
def get_attestations(self, uid: str = None, attester: str = None, recipient: str = None, schema_info: str = None, since: str = None, order: str = "desc", limit: int = 1000) -> dict:
return self.api.get_attestations(uid, attester, recipient, schema_info, since, order, limit)
def get_trust_lists(self, uid: str = None, attester: str = None, order: str = "desc", limit: int = 100) -> dict:
return self.api.get_trust_lists(uid, attester, order, limit)
# OLI Label Pool API functions (api key required)
def get_labels(self, address: str, chain_id: str=None, limit: int=1000, include_all: bool=False) -> dict:
return self.api.get_labels(address, chain_id, limit, include_all)
# Expose validation methods
def validate_label_correctness(self, address: str, chain_id: str, tags: dict, ref_uid: str="0x0000000000000000000000000000000000000000000000000000000000000000", auto_fix: bool=True) -> bool:
return self.validator.validate_label_correctness(address, chain_id, tags, ref_uid, auto_fix)
def get_trusted_labels(self, address: str, chain_id: str=None, limit: int=1000, include_all: bool=False, min_confidence: float=-1) -> dict:
response = self.api.get_labels(address, chain_id, limit, include_all)
filtered_labels = []
for label in response['labels']:
label['confidence'] = self.utils_data.get_confidence(label['attester'], label['tag_id'], label['chain_id'])
if label['confidence'] >= min_confidence or min_confidence == -1:
filtered_labels.append(label)
response['labels'] = filtered_labels
response['count_trusted'] = len(filtered_labels)
return response
def fix_simple_tags_formatting(self, tags: dict) -> dict:
return self.validator.fix_simple_tags_formatting(tags)
def get_labels_bulk(self, addresses: list, chain_id: str = None, limit_per_address: int = 1000, include_all: bool = False) -> dict:
return self.api.get_labels_bulk(addresses, chain_id, limit_per_address, include_all)
def get_trusted_labels_bulk(self, addresses: list, chain_id: str = None, limit_per_address: int = 1000, include_all: bool = False, min_confidence: float = -1) -> dict:
response = self.api.get_labels_bulk(addresses, chain_id, limit_per_address, include_all)
for i, address in enumerate(response['results']):
filtered_labels = []
labels = address['labels']
for label in labels:
label['confidence'] = self.utils_data.get_confidence(label['attester'], label['tag_id'], label['chain_id'])
if label['confidence'] >= min_confidence or min_confidence == -1:
filtered_labels.append(label)
response['results'][i] = {'address': address['address'], 'labels': filtered_labels}
response['results'][i]['count'] = len(labels)
response['results'][i]['count_trusted'] = len(filtered_labels)
return response
def search_addresses_by_tag(self, tag_id: str, tag_value: str, chain_id: str = None, limit: int = 1000) -> dict:
return self.api.search_addresses_by_tag(tag_id, tag_value, chain_id, limit)
def search_trusted_addresses_by_tag(self, tag_id: str, tag_value: str, chain_id: str = None, limit: int = 1000, min_confidence: float = -1) -> dict:
response = self.api.search_addresses_by_tag(tag_id, tag_value, chain_id, limit)
filtered_addresses = []
for label in response['results']:
label['confidence'] = self.utils_data.get_confidence(label['attester'], tag_id, label['chain_id'])
if label['confidence'] >= min_confidence or min_confidence == -1:
filtered_addresses.append(label)
response['results'] = filtered_addresses
response['count_trusted'] = len(filtered_addresses)
return response
def get_attester_analytics(self, chain_id: str = None, limit: int = 100) -> dict:
return self.api.get_attester_analytics(chain_id, limit)

@@ -1,4 +0,5 @@

from oli.data.graphql import GraphQLClient
from oli.data.fetcher import DataFetcher
from oli.data.api import API
from oli.data.trust import UtilsTrust
from oli.data.utils import UtilsData
__all__ = ["GraphQLClient", "DataFetcher"]
__all__ = ["API", "UtilsTrust", "UtilsData"]
+179
-56
Metadata-Version: 2.4
Name: oli-python
Version: 1.2.1
Version: 2.0.0
Summary: Python SDK for interacting with the Open Labels Initiative; A standard, registry and trust layer for EVM address labels.
Home-page: https://github.com/openlabelsinitiative/oli-python
Author: Lorenz Lehmann
Author-email: lorenz@growthepie.xyz
Author-email: lorenz@growthepie.com
Classifier: Programming Language :: Python :: 3

@@ -28,6 +28,8 @@ Classifier: License :: OSI Approved :: MIT License

# OLI Python Package
# OLI Python SDK
Python SDK for interacting with the Open Labels Initiative; A framework for address labels in the blockchain space. Read & write labels into the OLI Label Pool, check your labels for OLI compliance.
**`oli-python`** is the official Python SDK for the **[Open Labels Initiative (OLI)](https://github.com/openlabelsinitiative/OLI)**, a framework for standardized and trusted blockchain address labels.
This SDK lets you read, write, validate, and revoke address labels within the **OLI Label Pool** and compute confidence scores based on attesters' relationships within the **OLI Label Trust**.
## Installation

@@ -39,3 +41,3 @@

### Submitting New Labels
## Initialization

@@ -47,87 +49,208 @@ ```python

# Initialize the client
# Make sure to pull in your private key from an .env file
oli = OLI(private_key=os.environ['OLI_PRIVATE_KEY'], is_production=True)
oli = OLI(
private_key=os.getenv("PRIVATE_KEY"),
api_key=os.getenv("OLI_API_KEY")
)
```
# Create a label
**Setup Instructions:**
* Create a `.env` file to store your private variables
* Load an EOA wallet by setting the `private_key` variable
* The EOA wallet is used to track your reputation within OLI
* To access all advanced API endpoints, pass an `api_key` variable. You can get your free API key from **[here](http://openlabelsinitiative.org/developer)**
**Additional Options:**
You can also set the `chain` parameter to specify which EAS contracts location to use and the `custom_rpc_url` variable when initializing.
## Submit Labels into the OLI Label Pool
### Single Attestation
```python
# Define your OLI compliant attestation
address = "0x9438b8B447179740cD97869997a2FCc9b4AA63a2"
chain_id = "eip155:1" # Ethereum Mainnet
chain_id = "eip155:1" # Ethereum mainnet
tags = {
"contract_name": "growthepie donation address",
"is_eoa": True,
"owner_project": "growthepie"
"owner_project": "growthepie",
}
# Validate if your label is OLI compliant
possible_to_attest = oli.validate_label_correctness(address, chain_id, tags)
print(f"You can attest your label: {possible_to_attest}")
# (Optional) Validate your attestation before submission
is_valid = oli.validate_label(address, chain_id, tags)
print("Your attestation is OLI compliant:", is_valid)
# Submit a label as an offchain attestation
response = oli.submit_offchain_label(address, chain_id, tags)
print(f"Label submitted offchain with response: {response.text}")
# Submit label to the OLI Label Pool
response = oli.submit_label(address, chain_id, tags)
print(response)
```
# Submit a label as an onchain attestation
tx_hash, uid = oli.submit_onchain_label(address, chain_id, tags)
print(f"Label submitted onchain with hash: {tx_hash} and uid: {uid}")
### Bulk Attestation
# Batch submit multiple labels as one onchain attestation
labels = [
{'address': address, 'chain_id': chain_id, 'tags': tags},
{'address': address, 'chain_id': chain_id, 'tags': tags}
```python
attestations = [
{"address": address, "chain_id": chain_id, "tags": tags},
{"address": address, "chain_id": chain_id, "tags": tags},
]
tx_hash, uids = oli.submit_multi_onchain_labels(labels)
print(f"Labels batch submitted onchain with transaction hash: {tx_hash} and uids: {uids}")
# Revoke an attestation (revoking onchain attestations here)
trx_hash = oli.revoke_attestation(uid, onchain=True)
print(f"Label {uid} revoked with hash: {trx_hash}")
response = oli.submit_label_bulk(attestations)
print(response)
```
# Revoke multiple attestations (revoking onchain attestations here)
trx_hash, count = oli.multi_revoke_attestations(uids, onchain=True)
print(f"Labels batch revoked with hash: {trx_hash}")
## Revoke Attestations
**Note:** All revocations are onchain transactions and therefore require you to have ETH on Base for gas in your wallet.
```python
# Revoke a single attestation
oli.revoke_by_uid(uid="0x...")
# Bulk revocation
uids = ["0x1...", "0x2..."]
oli.revoke_bulk_by_uids(uids)
```
### Querying Existing Labels
## Reading the OLI Label Pool
### Get Raw Attestations
```python
from oli import OLI
import os
response = oli.get_attestations()
print(response)
```
# Initialize the client (read mode only doesn't require a private key)
oli = OLI()
**Available filters:** uid, attester, recipient, schema_info, since, order, limit
# Query attestations for a specific address
result = oli.graphql_query_attestations(address=address)
print(result)
### Get Labels for an Address
# Download parquet export of raw attestations
oli.get_full_raw_export_parquet()
```python
# All labels for an address
response = oli.get_labels(address = "0x9438b8B447179740cD97869997a2FCc9b4AA63a2")
print(response)
```
# Download parquet export of decoded attestations
oli.get_full_decoded_export_parquet()
**Available filters:** address, chain_id, limit, include_all
### Bulk Get Labels for Addresses
```python
response = ["0x9438b8B447179740cD97869997a2FCc9b4AA63a2", "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24"]
bulk = oli.get_labels_bulk(response)
print(bulk)
```
**Available filters:** addresses, chain_id, limit_per_address, include_all
## Wallet Requirements
### Get Addresses Based on a Tag
The [OLI Label Pool](https://github.com/openlabelsinitiative/OLI/tree/main/2_label_pool) lives on Base as an [Ethereum Attestation schema](https://base.easscan.org/schema/view/0xb763e62d940bed6f527dd82418e146a904e62a297b8fa765c9b3e1f0bc6fdd68).
```python
response = oli.search_addresses_by_tag(
tag_id="owner_project",
tag_value="growthepie"
)
print(response)
```
Make sure your wallet contains ETH on **Base Mainnet** to pay for onchain transaction (including offchain revocations). Offchain attestations are free.
**Available filters:** tag_id, tag_value, chain_id, limit
For testing purposes, you can use Base Sepolia Testnet by setting `is_production=False` when initializing the client.
### Attester Leaderboard
## Features
```python
chain_id = "eip155:1"
stats = oli.get_attester_analytics(chain_id)
print(stats)
```
- Submit onchain (single or batch) and offchain (single) labels into the OLI Label Pool
- Revoke your own labels (single or batch)
- Validate if your label is OLI compliant
- Query attestations using GraphQL
- Download full dataset exports in Parquet format
## OLI Label Trust
## Documentation
```python
# Set a new source node for trust propagation
trust_table = oli.set_trust_node("0xYourAddress")
print(oli.trust.trust_table)
For more details, see the [OLI Documentation](https://github.com/openlabelsinitiative/OLI).
# Import your trust list from YAML file
import yaml
with open('example_trust_list.yml', 'r') as file:
trust_list = yaml.safe_load(file)
owner_name = trust_list.get('owner_name')
attesters = trust_list.get('attesters', [])
attestations = trust_list.get('attestations', [])
# Check if the trust list is OLI compliant
is_valid = oli.validate_trust_list(
owner_name,
attesters,
attestations
)
print("Your trust list is OLI compliant:", is_valid)
# Add a private trust list to the trust graph
oli.add_trust_list(
owner_name,
attesters,
attestations
)
# Submit your trust list to the global trust graph
response = oli.submit_trust_list(
owner_name,
attesters,
attestations
)
print(response)
```
Once you set your trust node or submit your trust list, the transitive trust algorithm can assign a trust score to all labels. Use the following endpoints, which now assign a confidence score to each label. You can also set `min_confidence` to filter based on confidence thresholds.
### Get Trusted Labels for an Address
```python
# All trusted labels for an address
response = oli.get_trusted_labels(address = "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24")
print(response)
```
**Available filters:** address, chain_id, limit, include_all, min_confidence
### Bulk Get Trusted Labels for Addresses
```python
addresses = ["0x9438b8B447179740cD97869997a2FCc9b4AA63a2", "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24"]
response = oli.get_trusted_labels_bulk(addresses)
print(response)
```
**Available filters:** addresses, chain_id, limit_per_address, include_all, min_confidence
### Get Trusted Addresses Based on a Tag
```python
response = oli.search_trusted_addresses_by_tag(
tag_id = "owner_project",
tag_value = "growthepie"
)
print(response)
```
**Available filters:** tag_id, tag_value, chain_id, limit, min_confidence
## OLI Label Pool Parquet Exports
growthepie provides dataset exports as Parquet files for all attestations inside the OLI Label Pool, updated daily:
```python
oli.get_full_raw_export_parquet()
oli.get_full_decoded_export_parquet()
```
**Option:** Pass the filepath as a variable to store the export in a specific location.
## OLI Documentation
* [Open Labels Initiative Docs](https://github.com/openlabelsinitiative/OLI)
* [OLI Website](https://www.openlabelsinitiative.org/)
## License
MIT
MIT © Open Labels Initiative
+177
-54

@@ -1,5 +0,7 @@

# OLI Python Package
# OLI Python SDK
Python SDK for interacting with the Open Labels Initiative; A framework for address labels in the blockchain space. Read & write labels into the OLI Label Pool, check your labels for OLI compliance.
**`oli-python`** is the official Python SDK for the **[Open Labels Initiative (OLI)](https://github.com/openlabelsinitiative/OLI)**, a framework for standardized and trusted blockchain address labels.
This SDK lets you read, write, validate, and revoke address labels within the **OLI Label Pool** and compute confidence scores based on attesters' relationships within the **OLI Label Trust**.
## Installation

@@ -11,3 +13,3 @@

### Submitting New Labels
## Initialization

@@ -19,87 +21,208 @@ ```python

# Initialize the client
# Make sure to pull in your private key from an .env file
oli = OLI(private_key=os.environ['OLI_PRIVATE_KEY'], is_production=True)
oli = OLI(
private_key=os.getenv("PRIVATE_KEY"),
api_key=os.getenv("OLI_API_KEY")
)
```
# Create a label
**Setup Instructions:**
* Create a `.env` file to store your private variables
* Load an EOA wallet by setting the `private_key` variable
* The EOA wallet is used to track your reputation within OLI
* To access all advanced API endpoints, pass an `api_key` variable. You can get your free API key from **[here](http://openlabelsinitiative.org/developer)**
**Additional Options:**
You can also set the `chain` parameter to specify which EAS contracts location to use and the `custom_rpc_url` variable when initializing.
## Submit Labels into the OLI Label Pool
### Single Attestation
```python
# Define your OLI compliant attestation
address = "0x9438b8B447179740cD97869997a2FCc9b4AA63a2"
chain_id = "eip155:1" # Ethereum Mainnet
chain_id = "eip155:1" # Ethereum mainnet
tags = {
"contract_name": "growthepie donation address",
"is_eoa": True,
"owner_project": "growthepie"
"owner_project": "growthepie",
}
# Validate if your label is OLI compliant
possible_to_attest = oli.validate_label_correctness(address, chain_id, tags)
print(f"You can attest your label: {possible_to_attest}")
# (Optional) Validate your attestation before submission
is_valid = oli.validate_label(address, chain_id, tags)
print("Your attestation is OLI compliant:", is_valid)
# Submit a label as an offchain attestation
response = oli.submit_offchain_label(address, chain_id, tags)
print(f"Label submitted offchain with response: {response.text}")
# Submit label to the OLI Label Pool
response = oli.submit_label(address, chain_id, tags)
print(response)
```
# Submit a label as an onchain attestation
tx_hash, uid = oli.submit_onchain_label(address, chain_id, tags)
print(f"Label submitted onchain with hash: {tx_hash} and uid: {uid}")
### Bulk Attestation
# Batch submit multiple labels as one onchain attestation
labels = [
{'address': address, 'chain_id': chain_id, 'tags': tags},
{'address': address, 'chain_id': chain_id, 'tags': tags}
```python
attestations = [
{"address": address, "chain_id": chain_id, "tags": tags},
{"address": address, "chain_id": chain_id, "tags": tags},
]
tx_hash, uids = oli.submit_multi_onchain_labels(labels)
print(f"Labels batch submitted onchain with transaction hash: {tx_hash} and uids: {uids}")
# Revoke an attestation (revoking onchain attestations here)
trx_hash = oli.revoke_attestation(uid, onchain=True)
print(f"Label {uid} revoked with hash: {trx_hash}")
response = oli.submit_label_bulk(attestations)
print(response)
```
# Revoke multiple attestations (revoking onchain attestations here)
trx_hash, count = oli.multi_revoke_attestations(uids, onchain=True)
print(f"Labels batch revoked with hash: {trx_hash}")
## Revoke Attestations
**Note:** All revocations are onchain transactions and therefore require you to have ETH on Base for gas in your wallet.
```python
# Revoke a single attestation
oli.revoke_by_uid(uid="0x...")
# Bulk revocation
uids = ["0x1...", "0x2..."]
oli.revoke_bulk_by_uids(uids)
```
### Querying Existing Labels
## Reading the OLI Label Pool
### Get Raw Attestations
```python
from oli import OLI
import os
response = oli.get_attestations()
print(response)
```
# Initialize the client (read mode only doesn't require a private key)
oli = OLI()
**Available filters:** uid, attester, recipient, schema_info, since, order, limit
# Query attestations for a specific address
result = oli.graphql_query_attestations(address=address)
print(result)
### Get Labels for an Address
# Download parquet export of raw attestations
oli.get_full_raw_export_parquet()
```python
# All labels for an address
response = oli.get_labels(address = "0x9438b8B447179740cD97869997a2FCc9b4AA63a2")
print(response)
```
# Download parquet export of decoded attestations
oli.get_full_decoded_export_parquet()
**Available filters:** address, chain_id, limit, include_all
### Bulk Get Labels for Addresses
```python
response = ["0x9438b8B447179740cD97869997a2FCc9b4AA63a2", "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24"]
bulk = oli.get_labels_bulk(response)
print(bulk)
```
**Available filters:** addresses, chain_id, limit_per_address, include_all
## Wallet Requirements
### Get Addresses Based on a Tag
The [OLI Label Pool](https://github.com/openlabelsinitiative/OLI/tree/main/2_label_pool) lives on Base as an [Ethereum Attestation schema](https://base.easscan.org/schema/view/0xb763e62d940bed6f527dd82418e146a904e62a297b8fa765c9b3e1f0bc6fdd68).
```python
response = oli.search_addresses_by_tag(
tag_id="owner_project",
tag_value="growthepie"
)
print(response)
```
Make sure your wallet contains ETH on **Base Mainnet** to pay for onchain transaction (including offchain revocations). Offchain attestations are free.
**Available filters:** tag_id, tag_value, chain_id, limit
For testing purposes, you can use Base Sepolia Testnet by setting `is_production=False` when initializing the client.
### Attester Leaderboard
## Features
```python
chain_id = "eip155:1"
stats = oli.get_attester_analytics(chain_id)
print(stats)
```
- Submit onchain (single or batch) and offchain (single) labels into the OLI Label Pool
- Revoke your own labels (single or batch)
- Validate if your label is OLI compliant
- Query attestations using GraphQL
- Download full dataset exports in Parquet format
## OLI Label Trust
## Documentation
```python
# Set a new source node for trust propagation
trust_table = oli.set_trust_node("0xYourAddress")
print(oli.trust.trust_table)
For more details, see the [OLI Documentation](https://github.com/openlabelsinitiative/OLI).
# Import your trust list from YAML file
import yaml
with open('example_trust_list.yml', 'r') as file:
trust_list = yaml.safe_load(file)
owner_name = trust_list.get('owner_name')
attesters = trust_list.get('attesters', [])
attestations = trust_list.get('attestations', [])
# Check if the trust list is OLI compliant
is_valid = oli.validate_trust_list(
owner_name,
attesters,
attestations
)
print("Your trust list is OLI compliant:", is_valid)
# Add a private trust list to the trust graph
oli.add_trust_list(
owner_name,
attesters,
attestations
)
# Submit your trust list to the global trust graph
response = oli.submit_trust_list(
owner_name,
attesters,
attestations
)
print(response)
```
Once you set your trust node or submit your trust list, the transitive trust algorithm can assign a trust score to all labels. Use the following endpoints, which now assign a confidence score to each label. You can also set `min_confidence` to filter based on confidence thresholds.
### Get Trusted Labels for an Address
```python
# All trusted labels for an address
response = oli.get_trusted_labels(address = "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24")
print(response)
```
**Available filters:** address, chain_id, limit, include_all, min_confidence
### Bulk Get Trusted Labels for Addresses
```python
addresses = ["0x9438b8B447179740cD97869997a2FCc9b4AA63a2", "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24"]
response = oli.get_trusted_labels_bulk(addresses)
print(response)
```
**Available filters:** addresses, chain_id, limit_per_address, include_all, min_confidence
### Get Trusted Addresses Based on a Tag
```python
response = oli.search_trusted_addresses_by_tag(
tag_id = "owner_project",
tag_value = "growthepie"
)
print(response)
```
**Available filters:** tag_id, tag_value, chain_id, limit, min_confidence
## OLI Label Pool Parquet Exports
growthepie provides dataset exports as Parquet files for all attestations inside the OLI Label Pool, updated daily:
```python
oli.get_full_raw_export_parquet()
oli.get_full_decoded_export_parquet()
```
**Option:** Pass the filepath as a variable to store the export in a specific location.
## OLI Documentation
* [Open Labels Initiative Docs](https://github.com/openlabelsinitiative/OLI)
* [OLI Website](https://www.openlabelsinitiative.org/)
## License
MIT
MIT © Open Labels Initiative

@@ -8,5 +8,5 @@ from setuptools import setup, find_packages

name="oli-python",
version="1.2.1",
version="2.0.0",
author="Lorenz Lehmann",
author_email="lorenz@growthepie.xyz",
author_email="lorenz@growthepie.com",
description="Python SDK for interacting with the Open Labels Initiative; A standard, registry and trust layer for EVM address labels.",

@@ -13,0 +13,0 @@ long_description=long_description,

import requests
import yaml
class DataFetcher:
def __init__(self, oli_client):
"""
Initialize the DataFetcher with an OLI client.
Args:
oli_client: The OLI client instance
"""
self.oli = oli_client
def get_OLI_tags(self):
"""
Get latest OLI tags from OLI Github repo.
Returns:
dict: Dictionary of official OLI tags
"""
url = "https://raw.githubusercontent.com/openlabelsinitiative/OLI/refs/heads/main/1_label_schema/tags/tag_definitions.yml"
response = requests.get(url)
if response.status_code == 200:
y = yaml.safe_load(response.text)
y = {i['tag_id']: i for i in y['tags']}
return y
else:
raise Exception(f"Failed to fetch OLI tags from Github: {response.status_code} - {response.text}")
def get_OLI_value_sets(self) -> dict:
"""
Get latest value sets for OLI tags.
Returns:
dict: Dictionary of value sets with tag_id as key
"""
value_sets = {}
# Extract value sets from tag definitions (must be a list)
for tag_def in self.oli.tag_definitions.values():
if 'schema' not in tag_def:
continue
schema = tag_def['schema']
tag_id = tag_def['tag_id']
value_set = None
# Get enum from direct schema or array items
if 'enum' in schema:
value_set = schema['enum']
elif (schema.get('type') == 'array' and
'items' in schema and
'enum' in schema['items']):
value_set = schema['items']['enum']
# Process and add to value_sets
if value_set and isinstance(value_set, list):
value_sets[tag_id] = [i.lower() if isinstance(i, str) else i for i in value_set]
# value set for owner_project
url = "https://api.growthepie.com/v1/labels/projects.json"
response = requests.get(url)
if response.status_code == 200:
y = yaml.safe_load(response.text)
value_sets["owner_project"] = [i[0] for i in y['data']['data']]
value_sets["owner_project"] = [i.lower() if isinstance(i, str) else i for i in value_sets["owner_project"]]
else:
raise Exception(f"Failed to fetch owner_project value set from grwothepie projects api: {response.status_code} - {response.text}")
# value set for usage_category
url = "https://raw.githubusercontent.com/openlabelsinitiative/OLI/refs/heads/main/1_label_schema/tags/valuesets/usage_category.yml"
response = requests.get(url)
if response.status_code == 200:
y = yaml.safe_load(response.text)
value_sets['usage_category'] = [i['category_id'] for i in y['categories']]
value_sets['usage_category'] = [i.lower() if isinstance(i, str) else i for i in value_sets['usage_category']]
else:
raise Exception(f"Failed to fetch usage_category value set from OLI Github: {response.status_code} - {response.text}")
return value_sets
def get_full_raw_export_parquet(self, file_path: str="raw_labels.parquet") -> str:
"""
Downloads the full raw export of all labels in the OLI Label Pool as a Parquet file.
Args:
file_path (str): Path where the file will be saved. Defaults to "raw_labels.parquet".
Returns:
str: Path to the downloaded Parquet file
"""
url = "https://api.growthepie.com/v1/oli/labels_raw.parquet"
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(file_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded and saved: {file_path}")
return file_path
else:
print(f"Failed to download {url}. Status code: {response.status_code}")
return None
def get_full_decoded_export_parquet(self, file_path: str="decoded_labels.parquet") -> str:
"""
Downloads the full decoded export of all labels in the OLI Label Pool as a Parquet file.
Args:
file_path (str): Path where the file will be saved. Defaults to "decoded_labels.parquet".
Returns:
str: Path to the downloaded Parquet file
"""
url = "https://api.growthepie.com/v1/oli/labels_decoded.parquet"
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(file_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded and saved: {file_path}")
return file_path
else:
print(f"Failed to download {url}. Status code: {response.status_code}")
return None
import requests
import json
class GraphQLClient:
def __init__(self, oli_client):
"""
Initialize the GraphQLClient with an OLI client.
Args:
oli_client: The OLI client instance
"""
self.oli = oli_client
def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None, take: int=None, id: str=None, expand_json: bool=True) -> dict:
"""
Queries attestations from the EAS GraphQL API based on the specified filters.
Args:
address (str, optional): Ethereum address of the labeled contract
attester (str, optional): Ethereum address of the attester
timeCreated (int, optional): Filter for attestations created after this timestamp
revocationTime (int, optional): Filter for attestations with revocation time >= this timestamp
take (int, optional): Maximum number of attestations to return
id (str, optional): Specific attestation ID to filter by
expand_json (bool, default: True): Whether to expand decodedDataJson fields in the response
Returns:
dict: JSON response containing matching attestation data
"""
query = """
query Attestations($take: Int, $where: AttestationWhereInput, $orderBy: [AttestationOrderByWithRelationInput!]) {
attestations(take: $take, where: $where, orderBy: $orderBy) {
attester
decodedDataJson
expirationTime
id
ipfsHash
isOffchain
recipient
refUID
revocable
revocationTime
revoked
time
timeCreated
txid
}
}
"""
variables = {
"where": {
"schemaId": {
"equals": self.oli.oli_label_pool_schema
}
},
"orderBy": [
{
"timeCreated": "desc"
}
]
}
# Add take to variables if not None
if take is not None:
variables["take"] = int(take)
# Add id to where clause if not None
if id is not None:
variables["where"]["id"] = {"equals": id}
# Add address to where clause if not None
if address is not None:
variables["where"]["recipient"] = {"equals": address}
# Add attester to where clause if not None
if attester is not None:
variables["where"]["attester"] = {"equals": attester}
# Add timeCreated to where clause if not None, ensuring it's an int
if timeCreated is not None:
timeCreated = int(timeCreated)
variables["where"]["timeCreated"] = {"gt": timeCreated}
# Add revocationTime to where clause if not None, ensuring it's an int
if revocationTime is not None:
revocationTime = int(revocationTime)
variables["where"]["revocationTime"] = {"gte": revocationTime}
headers = {
"Content-Type": "application/json"
}
response = requests.post(self.oli.graphql, json={"query": query, "variables": variables}, headers=headers)
if response.status_code == 200:
if expand_json:
# Expand decodedDataJson fields in the response
return self.graphql_expand_decoded_data_json(response.json())
else:
# Return raw response if no expansion is wanted
return response.json()
else:
raise Exception(f"GraphQL query failed with status code {response.status_code}: {response.text}")
def graphql_expand_decoded_data_json(self, attestations_data: dict) -> list:
"""
Expand decodedDataJson fields in attestations data into separate columns.
Args:
attestations_data (dict): GraphQL response from oli.graphql_query_attestations()
Returns:
list: List of dictionaries with expanded decodedDataJson fields
"""
expanded_data = []
for row in attestations_data['data']['attestations']:
# Start with the original row data
expanded_row = row.copy()
# Check if decodedDataJson exists and is not None
if 'decodedDataJson' in row and row['decodedDataJson']:
try:
# Parse the JSON string
if isinstance(row['decodedDataJson'], str):
decoded_data = json.loads(row['decodedDataJson'])
else:
decoded_data = row['decodedDataJson']
# Extract each field from the decoded data
for item in decoded_data:
field_name = item['name']
# Extract the actual value from the nested structure
if 'value' in item and 'value' in item['value']:
value = item['value']['value']
# Handle BigNumber hex values
if isinstance(value, dict) and value.get('type') == 'BigNumber':
expanded_row[field_name] = int(value['hex'], 16)
# Handle empty arrays or objects
elif isinstance(value, (list, dict)) and not value:
expanded_row[field_name] = value
else:
expanded_row[field_name] = value
else:
expanded_row[field_name] = None
except (json.JSONDecodeError, KeyError, TypeError) as e:
# If parsing fails, keep original row and add error info
expanded_row['_parsing_error'] = str(e)
expanded_data.append(expanded_row)
return {'data': {'attestations': expanded_data}}