|
import requests |
|
import pandas as pd |
|
import gradio as gr |
|
import plotly.express as px |
|
from datetime import datetime, timedelta |
|
import plotly.graph_objects as go |
|
import numpy as np |
|
import json |
|
from web3 import Web3 |
|
import time |
|
import os |
|
from itertools import product |
|
|
|
from dotenv import load_dotenv |
|
load_dotenv() |
|
|
|
OPTIMISM_RPC_URL = os.getenv('OPTIMISM_RPC_URL') |
|
BASE_RPC_URL = os.getenv('BASE_RPC_URL') |
|
ETH_RPC_URL = os.getenv('ETH_RPC_URL') |
|
MODE_RPC_URL = os.getenv('MODE_RPC_URL') |
|
|
|
print(f"Optimism RPC URL: {OPTIMISM_RPC_URL}") |
|
|
|
|
|
print("Initializing Web3 instances...") |
|
web3_optimism = Web3(Web3.HTTPProvider(OPTIMISM_RPC_URL)) |
|
web3_base = Web3(Web3.HTTPProvider(BASE_RPC_URL)) |
|
web3_eth = Web3(Web3.HTTPProvider(ETH_RPC_URL)) |
|
web3_mode = Web3(Web3.HTTPProvider(MODE_RPC_URL)) |
|
|
|
|
|
contract_address_optimism = '0x3d77596beb0f130a4415df3D2D8232B3d3D31e44' |
|
contract_address_base = '0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE' |
|
contract_address_eth = '0x48b6af7B12C71f09e2fC8aF4855De4Ff54e775cA' |
|
contract_address_mode = '0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE' |
|
|
|
|
|
with open('./contracts/service_registry_abi.json', 'r') as abi_file: |
|
contract_abi = json.load(abi_file) |
|
|
|
|
|
service_registry_optimism = web3_optimism.eth.contract(address=contract_address_optimism, abi=contract_abi) |
|
service_registry_base = web3_base.eth.contract(address=contract_address_base, abi=contract_abi) |
|
service_registry_eth = web3_eth.eth.contract(address=contract_address_eth, abi=contract_abi) |
|
service_registry_mode = web3_mode.eth.contract(address=contract_address_mode, abi=contract_abi) |
|
print("Service registry contracts loaded.") |
|
|
|
|
|
if not web3_optimism.is_connected(): |
|
raise Exception("Failed to connect to the Optimism network.") |
|
if not web3_base.is_connected(): |
|
raise Exception("Failed to connect to the Base network.") |
|
if not web3_eth.is_connected(): |
|
raise Exception("Failed to connect to the ETH network.") |
|
if not web3_mode.is_connected(): |
|
raise Exception("Failed to connect to the Mode network.") |
|
print("Successfully connected to Ethereum, Optimism, Base, and Mode networks.") |
|
|
|
|
|
|
|
def fetch_service_safes(web3, registry_contract): |
|
print("\nFetching service safes...") |
|
total_services = registry_contract.functions.totalSupply().call() |
|
print(f"Total services: {total_services}") |
|
service_safes_dict = {} |
|
zero_address = '0x0000000000000000000000000000000000000000' |
|
|
|
for service_id in range(1, total_services + 1): |
|
print(f"Processing service ID: {service_id}") |
|
service = registry_contract.functions.getService(service_id).call() |
|
agent_ids = service[-1] |
|
print(f"Agent IDs: {agent_ids}") |
|
|
|
if 40 in agent_ids or 25 in agent_ids: |
|
agent_instance_data = registry_contract.functions.getAgentInstances(service_id).call() |
|
service_safe = service[1] |
|
agent_addresses = agent_instance_data[1] |
|
if agent_addresses: |
|
agent_address = agent_addresses[0] |
|
if service_safe != zero_address: |
|
print(f"Found agent_address: {agent_address}") |
|
print(f"Found service safe: {service_safe}") |
|
service_safes_dict[agent_address] = service_safe |
|
else: |
|
print(f"Found zero address for service safe, skipping for agent: {agent_address}") |
|
else: |
|
print(f"No agent address found for service ID: {service_id}") |
|
|
|
print(f"Total unique agent addresses found (excluding zero safe addresses): {len(service_safes_dict)}") |
|
return service_safes_dict |
|
|
|
|
|
service_safes_optimism = fetch_service_safes(web3_optimism, service_registry_optimism) |
|
service_safes_base = fetch_service_safes(web3_base, service_registry_base) |
|
service_safes_eth = fetch_service_safes(web3_eth, service_registry_eth) |
|
service_safes_mode = fetch_service_safes(web3_mode, service_registry_mode) |
|
|
|
print(f"Service safes for Mode: {service_safes_mode}") |
|
def get_block_range_for_date(chain_id, date_str, api_key, base_url): |
|
"""Get the block range for a specific date.""" |
|
target_date = datetime.strptime(date_str, "%Y-%m-%d").date() |
|
start_of_day = datetime.combine(target_date, datetime.min.time()) |
|
|
|
if target_date == datetime.now().date(): |
|
end_of_day = datetime.now() |
|
else: |
|
end_of_day = datetime.combine(target_date, datetime.max.time()) |
|
|
|
start_timestamp = int(start_of_day.timestamp()) |
|
end_timestamp = int(end_of_day.timestamp()) |
|
|
|
|
|
start_response = requests.get( |
|
f"{base_url}?module=block&action=getblocknobytime×tamp={start_timestamp}&closest=before&apikey={api_key}" |
|
) |
|
if start_response.status_code == 200: |
|
start_data = start_response.json() |
|
start_block = start_data.get('result') |
|
else: |
|
print(f"Error fetching start block for {date_str} on chain {chain_id}") |
|
return None, None |
|
|
|
if start_block is None: |
|
print(f"No start block found for chain {chain_id} on {date_str}") |
|
return None, None |
|
print(f"Start block for chain {chain_id} on {date_str}: {start_block}") |
|
|
|
|
|
time.sleep(1) |
|
end_response = requests.get( |
|
f"{base_url}?module=block&action=getblocknobytime×tamp={end_timestamp}&closest=before&apikey={api_key}" |
|
) |
|
if end_response.status_code == 200: |
|
end_data = end_response.json() |
|
end_block = end_data.get('result') |
|
else: |
|
print(f"Error fetching end block for {date_str} on chain {chain_id}") |
|
return None, None |
|
|
|
if end_block is None: |
|
print(f"No end block found for chain {chain_id} on {date_str}") |
|
return None, None |
|
print(f"End block for chain {chain_id} on {date_str}: {end_block}") |
|
|
|
return start_block, end_block |
|
|
|
def get_transactions_mode_all(address): |
|
url = f'https://explorer.mode.network/api/v2/addresses/{address}/transactions' |
|
headers = { |
|
'accept': 'application/json' |
|
} |
|
|
|
response = requests.get(url, headers=headers) |
|
return response.json()['items'] |
|
|
|
def get_transactions(api_keys, wallet_address, chain_name, start_block, end_block): |
|
"""Retrieve transactions for the given wallet address, chain, and block range using the Etherscan or similar API.""" |
|
base_url = { |
|
'optimism': "https://api-optimistic.etherscan.io/api", |
|
'base': "https://api.basescan.org/api", |
|
'ethereum': "https://api.etherscan.io/api", |
|
'mode': "https://eth-sepolia.blockscout.com/api" |
|
}.get(chain_name) |
|
|
|
if not base_url: |
|
print(f"Invalid chain name: {chain_name}") |
|
return [] |
|
|
|
params = { |
|
'module': 'account', |
|
'action': 'txlist', |
|
'address': wallet_address, |
|
'startblock': start_block, |
|
'endblock': end_block, |
|
'sort': 'asc', |
|
'apikey': api_keys.get(chain_name) |
|
} |
|
|
|
response = requests.get(base_url, params=params) |
|
data = response.json() |
|
|
|
time.sleep(1) |
|
|
|
if data['status'] != '1': |
|
print(f"Error: {data['message']}") |
|
return [] |
|
|
|
valid_transactions = [tx for tx in data['result'] if tx['isError'] == '0'] |
|
|
|
return valid_transactions |
|
|
|
def get_mode_transactions(transactions_all_base,date_str): |
|
target_date = datetime.strptime(date_str, "%Y-%m-%d").date() |
|
start_of_day = datetime.combine(target_date, datetime.min.time()) |
|
|
|
if target_date == datetime.now().date(): |
|
end_of_day = datetime.now() |
|
else: |
|
end_of_day = datetime.combine(target_date, datetime.max.time()) |
|
|
|
start_timestamp = start_of_day.isoformat() |
|
end_timestamp = end_of_day.isoformat() |
|
processed_list = [] |
|
for transaction in transactions_all_base: |
|
|
|
if start_timestamp <= transaction['timestamp'] <= end_timestamp: |
|
transaction_timestamp = datetime.strptime(transaction['timestamp'], "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S") |
|
processed_transaction = { |
|
'hash': transaction['hash'], |
|
'timeStamp': transaction_timestamp, |
|
'from': transaction['from']['hash'], |
|
'to': transaction['to']['hash'], |
|
'value': transaction['value'] |
|
} |
|
processed_list.append(processed_transaction) |
|
|
|
return processed_list |
|
|
|
|
|
def date_range(start_date, end_date): |
|
"""Generates a range of dates from start_date to end_date inclusive.""" |
|
start_dt = datetime.strptime(start_date, "%Y-%m-%d") |
|
end_dt = datetime.strptime(end_date, "%Y-%m-%d") |
|
delta = timedelta(days=1) |
|
current_dt = start_dt |
|
while current_dt <= end_dt: |
|
yield current_dt.strftime("%Y-%m-%d") |
|
current_dt += delta |
|
|
|
def fetch_transactions(): |
|
api_keys = { |
|
'optimism': 'XQ72JA5XZ51QC7TG1W295AAIF4KTV92K1K', |
|
'base': '4BFQMVW1QUKEPVDA4VW711CF4462682CY8', |
|
'ethereum': '3GRYJGX55W3QWCEKGREF4H53AFHCAIVVR7', |
|
'mode': 'f651444d-2916-4f37-bfd8-e70cc8232eb7' |
|
} |
|
|
|
base_urls = { |
|
10: "https://api-optimistic.etherscan.io/api", |
|
8453: "https://api.basescan.org/api", |
|
1: "https://api.etherscan.io/api", |
|
3443: "https://eth-sepolia.blockscout.com/api" |
|
} |
|
|
|
current_date = datetime.today().strftime("%Y-%m-%d") |
|
csv_filename = 'daily_transactions_new.csv' |
|
|
|
if os.path.exists(csv_filename): |
|
df_existing = pd.read_csv(csv_filename) |
|
if 'date' in df_existing: |
|
last_date_in_csv = df_existing['date'].max() |
|
else: |
|
df_existing['date'] = pd.to_datetime(df_existing['timestamp']).dt.date |
|
last_date_in_csv = df_existing['date'].max() |
|
else: |
|
df_existing = pd.DataFrame() |
|
last_date_in_csv = '2024-09-19' |
|
|
|
start_date = (datetime.strptime(last_date_in_csv, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%d") |
|
|
|
chains = { |
|
10: ('optimism', service_safes_optimism), |
|
8453: ('base', service_safes_base), |
|
1: ('ethereum', service_safes_eth), |
|
3443: ('mode', service_safes_mode) |
|
} |
|
|
|
all_transactions = df_existing.to_dict('records') if not df_existing.empty else [] |
|
|
|
for chain_id, (chain_name, service_safes) in chains.items(): |
|
base_url = base_urls[chain_id] |
|
api_key = api_keys[chain_name] |
|
|
|
for agent_address, safe_address in service_safes.items(): |
|
print(f"\nProcessing {chain_name.capitalize()} for agent address {agent_address} (safe address {safe_address})...") |
|
if chain_name == 'mode': |
|
transactions_all_base = get_transactions_mode_all(safe_address) |
|
|
|
for single_date in date_range(start_date, current_date): |
|
if chain_name == 'mode': |
|
print(f"Processing date {single_date} for chain {chain_name}...") |
|
transactions = get_mode_transactions(transactions_all_base,single_date) |
|
|
|
else: |
|
print(f"Processing date {single_date} for chain {chain_name}...") |
|
start_block, end_block = get_block_range_for_date(chain_id, single_date, api_key, base_url) |
|
if start_block is None or end_block is None: |
|
print(f"Skipping date {single_date} for chain {chain_name} due to missing block data.") |
|
continue |
|
print(f"Start Block: {start_block}, End Block: {end_block} for date {single_date}") |
|
transactions = get_transactions(api_keys, safe_address, chain_name, start_block, end_block) |
|
|
|
if transactions: |
|
print(f"Found {len(transactions)} transactions on {single_date} for {chain_name.capitalize()} safe address {safe_address}:") |
|
for tx in transactions: |
|
if chain_name != 'mode': |
|
print(f"Transaction Hash: {tx['hash']} on {chain_name.capitalize()} at {tx['timeStamp']}") |
|
tx_time = datetime.fromtimestamp(int(tx['timeStamp'])) |
|
all_transactions.append({ |
|
'chain': chain_name, |
|
'agent_address': agent_address, |
|
'safe_address': safe_address, |
|
'date': single_date, |
|
'transaction_hash': tx['hash'], |
|
'timestamp': tx_time, |
|
'from': tx['from'], |
|
'to': tx['to'], |
|
'value_eth': int(tx['value']) / 1e18 |
|
}) |
|
else: |
|
all_transactions.append({ |
|
'chain': chain_name, |
|
'agent_address': agent_address, |
|
'safe_address': safe_address, |
|
'date': single_date, |
|
'transaction_hash': tx['hash'], |
|
'timestamp': tx['timeStamp'], |
|
'from': tx['from'], |
|
'to': tx['to'], |
|
'value_eth': int(tx['value']) / 1e18 |
|
}) |
|
|
|
else: |
|
print(f"No transactions found for agent address {agent_address} (safe address {safe_address}) on {single_date} on {chain_name.capitalize()}.") |
|
|
|
df_transactions_new = pd.DataFrame(all_transactions) |
|
df_transactions_new.to_csv(csv_filename, index=False) |
|
return df_transactions_new |
|
|
|
|
|
def create_transcation_visualizations(): |
|
df_transactions_new = fetch_transactions() |
|
df_transactions_new['timestamp'] = pd.to_datetime(df_transactions_new['timestamp']) |
|
|
|
|
|
daily_counts = df_transactions_new.groupby([df_transactions_new['timestamp'].dt.date, 'chain']).size().unstack(fill_value=0) |
|
|
|
|
|
chains = ['optimism', 'base', 'ethereum', 'mode'] |
|
for chain in chains: |
|
if chain not in daily_counts.columns: |
|
daily_counts[chain] = 0 |
|
|
|
daily_counts = daily_counts[chains] |
|
|
|
|
|
daily_counts['timestamp'] = daily_counts.index |
|
daily_counts['timestamp'] = pd.to_datetime(daily_counts['timestamp']) |
|
daily_counts = daily_counts.reset_index(drop=True) |
|
|
|
min_date = daily_counts['timestamp'].min() |
|
max_date = daily_counts['timestamp'].max() |
|
|
|
|
|
full_date_range = pd.date_range(start=min_date, end=max_date, freq='D') |
|
|
|
|
|
complete_df = pd.DataFrame({'timestamp': full_date_range}) |
|
complete_df = complete_df.merge(daily_counts, on='timestamp', how='left') |
|
complete_df = complete_df.fillna(0) |
|
daily_counts = complete_df |
|
|
|
daily_counts['timestamp'] = pd.to_datetime(daily_counts['timestamp']) |
|
|
|
|
|
new_rows = [] |
|
|
|
for _, row in daily_counts.iterrows(): |
|
|
|
slot1 = row.copy() |
|
|
|
|
|
|
|
new_rows.extend([slot1]) |
|
slot2 = row.copy() |
|
if slot2['timestamp'].dayofweek == 6: |
|
slot2[chains] = 0 |
|
slot2['timestamp'] = row['timestamp'].replace(hour=12) |
|
new_rows.extend([slot2]) |
|
|
|
|
|
|
|
|
|
hourly_counts = pd.DataFrame(new_rows).sort_values('timestamp') |
|
|
|
|
|
dates = hourly_counts['timestamp'].tolist() |
|
values = hourly_counts[chains].to_numpy() |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
date_objects = pd.to_datetime(dates) |
|
|
|
|
|
x_numeric = np.arange(len(dates)) |
|
|
|
|
|
width_array = [] |
|
|
|
for i, date in enumerate(date_objects): |
|
width_array.append(1.0) |
|
|
|
|
|
monday_indices = [i for i, date in enumerate(date_objects) if date.dayofweek == 0] |
|
monday_labels = [date_objects[i].strftime('%m-%d') for i in monday_indices] |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
name='Optimism', |
|
x=x_numeric, |
|
y=values[:,0], |
|
marker_color='blue', |
|
opacity=0.7, |
|
text=None, |
|
width=width_array, |
|
textposition='none', |
|
)) |
|
|
|
fig.add_trace(go.Bar( |
|
name='Base', |
|
x=x_numeric, |
|
y=values[:,1], |
|
marker_color='purple', |
|
opacity=0.7, |
|
text=None, |
|
textposition='none', |
|
width=width_array, |
|
)) |
|
|
|
fig.add_trace(go.Bar( |
|
name='Ethereum', |
|
x=x_numeric, |
|
y=values[:,2], |
|
marker_color='darkgreen', |
|
opacity=0.7, |
|
text=None, |
|
width=width_array, |
|
textposition='none', |
|
)) |
|
|
|
fig.add_trace(go.Bar( |
|
name='Mode', |
|
x=x_numeric, |
|
y=values[:,3], |
|
marker_color='orange', |
|
opacity=0.7, |
|
text=None, |
|
width=width_array, |
|
textposition='none', |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title='Chain Daily Activity : Transactions', |
|
xaxis_title='Date', |
|
yaxis_title='Daily Transactions Count', |
|
barmode='stack', |
|
showlegend=True, |
|
legend_title_text='Transaction Chain', |
|
height=600, |
|
bargap=0, |
|
bargroupgap=0, |
|
xaxis=dict( |
|
tickangle=-45, |
|
tickmode='array', |
|
ticktext=monday_labels, |
|
tickvals=monday_indices, |
|
), |
|
template='plotly_white', |
|
hoverlabel=dict( |
|
font_size=12, |
|
), |
|
) |
|
|
|
|
|
for trace in fig.data: |
|
trace.update( |
|
hovertemplate="<b>Date:</b> %{text}<br>" + |
|
"<b>" + trace.name + ":</b> %{y}<br>" + |
|
"<extra></extra>", |
|
text=[d.strftime('%Y-%m-%d') for d in date_objects] |
|
) |
|
|
|
|
|
return fig |
|
|
|
def create_active_agents_visualizations(): |
|
df_transactions_new = fetch_transactions() |
|
df_transactions_new['timestamp'] = pd.to_datetime(df_transactions_new['timestamp']) |
|
|
|
df_transactions_new['week_start'] = df_transactions_new['timestamp'].dt.to_period('W').apply(lambda r: r.start_time) |
|
df_transactions_new['weekday'] = df_transactions_new['timestamp'].dt.weekday |
|
|
|
|
|
daily_agents = df_transactions_new.groupby(['week_start', 'weekday'])['agent_address'].nunique().reset_index() |
|
|
|
|
|
today = datetime.today() |
|
yesterday = today - timedelta(days=1) |
|
valid_weekdays = [(ws, wd) for ws, wd in product(daily_agents['week_start'].unique(), range(7)) if ws + timedelta(days=wd) <= yesterday] |
|
|
|
all_combinations = pd.DataFrame(valid_weekdays, columns=['week_start', 'weekday']) |
|
|
|
|
|
daily_agents = all_combinations.merge(daily_agents, on=['week_start', 'weekday'], how='left').fillna(0) |
|
|
|
|
|
weekly_avg_agents = daily_agents.groupby('week_start')['agent_address'].mean().reset_index() |
|
weekly_avg_agents.rename(columns={'agent_address': 'avg_daily_active_agents'}, inplace=True) |
|
|
|
|
|
weeks = weekly_avg_agents['week_start'].unique() |
|
avg_agents_per_week = weekly_avg_agents['avg_daily_active_agents'] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
fig.add_trace(go.Bar( |
|
x=[f'{week.strftime("%b %d")}' for week in weeks], |
|
y=avg_agents_per_week, |
|
|
|
marker_color='blue', |
|
opacity=0.7, |
|
text=None, |
|
hoverlabel=dict( |
|
font_size=12, |
|
), |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title='Daily Active Agents: Weekly Average Number of Agents with at Least 1 Transaction Daily', |
|
xaxis_title='Week', |
|
yaxis_title='Average Number of Active Agents', |
|
xaxis=dict( |
|
tickangle=-45 |
|
), |
|
height=600, |
|
width=1000, |
|
bargap=0, |
|
bargroupgap=0.2, |
|
template='plotly_white' |
|
) |
|
|
|
return fig |
|
|
|
|
|
|
|
def dashboard(): |
|
with gr.Blocks() as demo: |
|
gr.Markdown("# Valory Transactions Dashboard") |
|
|
|
|
|
with gr.Tab("Transactions"): |
|
fig_tx_chain = create_transcation_visualizations() |
|
gr.Plot(fig_tx_chain) |
|
with gr.Tab("DAA"): |
|
fig_active_agents = create_active_agents_visualizations() |
|
gr.Plot(fig_active_agents) |
|
|
|
|
|
|
|
return demo |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
dashboard().launch() |