1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly .graph_objs as go
5
+ import plotly .express as px
6
+ import utils
7
+ import fundingutils
8
+
9
+
10
+ st .set_page_config (
11
+ page_title = "Cluster Match Results" ,
12
+ page_icon = "📊" ,
13
+ layout = "wide" ,
14
+ )
15
+
16
+ blockchain_mapping = {
17
+ 1 : "ethereum" ,
18
+ 10 : "optimism" ,
19
+ 137 : "polygon" ,
20
+ 250 : "fantom" ,
21
+ 324 : "zksync" ,
22
+ 8453 : "base" ,
23
+ 42161 : "arbitrum" ,
24
+ 43114 : "avalanche_c" ,
25
+ 534352 : "scroll"
26
+ }
27
+
28
+ if 'round_address' not in st .session_state :
29
+ st .session_state .round_address = None
30
+
31
+ # Grab round_address from URL
32
+ query_params = st .query_params .get_all ('round_address' )
33
+ if len (query_params ) == 1 and not st .session_state .round_address :
34
+ st .session_state .round_address = query_params [0 ]
35
+
36
+ round_address = st .session_state .round_address .lower ()
37
+
38
+ rounds = utils .get_round_summary ()
39
+ #st.write(rounds)
40
+ rounds = rounds [rounds ['round_id' ].str .lower () == round_address ]
41
+
42
+ round_name = rounds ['round_name' ].values [0 ]
43
+ matching_cap_amount = rounds ['matching_cap_amount' ].astype (float ).values [0 ] if 'matching_cap_amount' in rounds and not pd .isnull (rounds ['matching_cap_amount' ].values [0 ]) else 'No Cap'
44
+ matching_funds_available = rounds ['matching_funds_available' ].astype (float ).values [0 ] if 'matching_funds_available' in rounds else 0
45
+ min_donation_threshold_amount = rounds ['min_donation_threshold_amount' ].astype (float ).values [0 ] if 'min_donation_threshold_amount' in rounds and not pd .isnull (rounds ['min_donation_threshold_amount' ].values [0 ]) else 0.0
46
+ sybilDefense = rounds ['sybilDefense' ].values [0 ] if 'sybilDefense' in rounds else False
47
+ token = rounds ['token' ].values [0 ] if 'token' in rounds else 'ETH'
48
+ chain = blockchain_mapping .get (rounds ['chain_id' ].values [0 ] if 'chain_id' in rounds else 1 )
49
+
50
+ st .title (f'{ round_name } Cluster Match Results' )
51
+ st .header ('⚙️ Round Settings' )
52
+ col1 , col2 = st .columns (2 )
53
+ col1 .write (f"Matching Cap (%) Amount: { matching_cap_amount } " )
54
+ col2 .write (f"Matching Available: { matching_funds_available } " )
55
+ col2 .write (f"Minimum Donation Threshold Amount: { min_donation_threshold_amount } " )
56
+ col1 .write (f"Gitcoin Passport Used: { sybilDefense } " )
57
+
58
+
59
+
60
+ matching_amount = rounds ['matching_funds_available' ].astype (float ).values [0 ]
61
+ df = utils .get_round_votes (round_address )
62
+ #st.write(df)
63
+
64
+ unique_voters = df ['voter' ].nunique ()
65
+ col1 .write (f"Number of unique voters: { unique_voters } " )
66
+ col2 .write (f"Number of unique projects: { df ['project_name' ].nunique ()} " )
67
+ col1 .write (f"Chain: { chain } " )
68
+ col2 .write (f"Matching Token: { token } " )
69
+
70
+ if token == '0x7f9a7db853ca816b9a138aee3380ef34c437dee0' :
71
+ token = '0xde30da39c46104798bb5aa3fe8b9e0e1f348163f'
72
+ chain = 'ethereum'
73
+
74
+ with st .spinner ('Fetching token price...' ):
75
+ price_df = utils .get_token_price_from_dune (chain , token )
76
+
77
+ #st.write(price_df)
78
+ matching_token_price = price_df ['price' ].values [0 ]
79
+ matching_token_decimals = price_df ['decimals' ].values [0 ]
80
+ matching_token_symbol = price_df ['symbol' ].values [0 ]
81
+ col1 .write (f"Token Symbol: { matching_token_symbol } " )
82
+
83
+
84
+ votes_df = fundingutils .pivot_votes (df )
85
+
86
+ def get_matching (strategy , votes_df , matching_amount ):
87
+ df = fundingutils .get_qf_matching (strategy , votes_df , 100 , matching_amount , cluster_df = votes_df )
88
+ df = df .rename (columns = {'project_name' : 'Project' , 'matching_amount' : f'{ strategy } Match' , 'matching_percent' : f'{ strategy } Match %' })
89
+ return df
90
+
91
+ strategies = ['COCM' , 'qf' ]#, 'donation_profile_clustermatch', 'pairwise'] # Add or remove strategies as needed
92
+
93
+ votes_df = fundingutils .pivot_votes (df )
94
+ matching_dfs = [get_matching (strategy , votes_df , matching_amount ) for strategy in strategies ]
95
+
96
+ matching_df = matching_dfs [0 ]
97
+ for df in matching_dfs [1 :]:
98
+ matching_df = pd .merge (matching_df , df , on = 'Project' , how = 'outer' )
99
+
100
+
101
+ st .header ('💚 Quadratic Funding Results Comparison' )
102
+ st .write ('''Quadratic funding helps us solve coordination failures by creating a way for community members to fund what matters to them while amplifying their impact. However, it's assumption that people make independent decisions can be exploited to unfairly influence the distribution of matching funds.
103
+
104
+ Collusion-oriented cluster-matching (COCM) doesn’t make this assumption. Instead, it quantifies just how coordinated groups of actors are likely to be based on the social signals they have in common. Projects backed by more independent agents receive greater matching funds. Conversely, if a project’s support network shows higher levels of coordination, the matching funds are reduced, encouraging self-organized solutions within more coordinated groups.
105
+
106
+ ''' )
107
+
108
+
109
+ if 'qf' in strategies :
110
+ for strategy in strategies :
111
+ if strategy != 'qf' :
112
+ matching_df [f'{ strategy } _Diff' ] = abs (matching_df ['qf Match' ] - matching_df [f'{ strategy } Match' ])
113
+ st .metric (label = f"Matching Funds Redistributed by { strategy } " , value = round (matching_df [f'{ strategy } _Diff' ].sum (), 2 ))
114
+ st .metric (label = f"Percentage of Matching Funds Redistributed by { strategy } " , value = str (round (matching_df [f'{ strategy } _Diff' ].sum () / matching_amount * 100 , 2 )) + '%' )
115
+
116
+ st .write (matching_df )
117
+
118
+ output_df = matching_df [['Project' , 'COCM Match' ]]
119
+
120
+
121
+ projects_df = utils .get_projects_in_round (round_address )
122
+
123
+
124
+ output_df = pd .merge (output_df , projects_df , left_on = 'Project' , right_on = 'project_name' , how = 'outer' )
125
+ output_df = output_df .rename (columns = {'id' : 'applicationId' , 'project_id' :'projectId' , 'project_name' : 'projectName' , 'recipient_address' :'payoutAddress' , 'total_donations_count' :'contributionsCount' , 'COCM Match' : 'matched' , 'total_amount_donated_in_usd' :'totalReceived' })
126
+ output_df = output_df [['applicationId' , 'projectId' , 'projectName' , 'payoutAddress' , 'matched' , 'contributionsCount' , 'totalReceived' ]]
127
+ output_df ['matchedUSD' ] = output_df ['matched' ] * matching_token_price
128
+ output_df ['matched' ] = output_df ['matched' ] * 10 ** matching_token_decimals
129
+ output_df ['totalReceived' ] = output_df ['totalReceived' ] * 1e18
130
+
131
+ # Add additional columns
132
+ output_df ['matched' ] = output_df ['matched' ].apply (lambda x : '{:.0f}' .format (x ) if pd .notnull (x ) else x )
133
+ output_df ['totalReceived' ] = output_df ['totalReceived' ].apply (lambda x : '{:.0f}' .format (x ) if pd .notnull (x ) else x )
134
+
135
+
136
+ output_df ['sumOfSqrt' ] = 0
137
+ output_df ['capOverflow' ] = 0
138
+ output_df ['matchedWithoutCap' ] = 0
139
+ output_df = output_df [['applicationId' , 'projectId' , 'projectName' , 'payoutAddress' , 'matchedUSD' , 'totalReceived' , 'contributionsCount' , 'matched' , 'sumOfSqrt' , 'capOverflow' , 'matchedWithoutCap' ]]
140
+
141
+ st .header ('Download COCM Matching Results' )
142
+ st .write (output_df )
143
+ st .download_button (
144
+ label = "Download Output Data" ,
145
+ data = output_df .to_csv (index = False ),
146
+ file_name = 'output_data.csv' ,
147
+ mime = 'text/csv'
148
+ )
149
+ st .write ('You can upload this CSV to manager.gitcoin.co to apply the cluster matching results to your round' )
0 commit comments