Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): adds trusta labs stamp weight #350

Merged
merged 7 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions api/cgrants/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,21 @@ def _get_contributor_statistics_for_cgrants(


def _get_contributor_statistics_for_protocol(address: str) -> dict:
total_amount_usd = ProtocolContributions.objects.filter(
contributor=address
).aggregate(Sum("amount"))["amount__sum"]
num_rounds = ProtocolContributions.objects.filter(contributor=address).aggregate(
Count("round", distinct=True)
)["round__count"]
num_projects = ProtocolContributions.objects.filter(contributor=address).aggregate(
Count("project", distinct=True)
)["project__count"]
protocol_filter = ProtocolContributions.objects.filter(
contributor=address, amount__gte=1
)
total_amount_usd = protocol_filter.aggregate(Sum("amount"))["amount__sum"]
num_rounds = protocol_filter.aggregate(Count("round", distinct=True))[
"round__count"
]
num_projects = protocol_filter.aggregate(Count("project", distinct=True))[
"project__count"
]

return {
"num_grants_contribute_to": num_projects if num_projects is not None else 0,
"num_rounds_contribute_to": num_rounds if num_rounds is not None else 0,
"total_contribution_amount": total_amount_usd
"total_valid_contribution_amount": round(total_amount_usd, 3)
if total_amount_usd is not None
else 0,
"num_gr14_contributions": 0,
Expand Down
29 changes: 28 additions & 1 deletion api/cgrants/test/test_cgrants_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,34 @@ def test_contributor_statistics_with_only_protocol_contributions(self):
{
"num_grants_contribute_to": 4,
"num_rounds_contribute_to": 2,
"total_contribution_amount": "10",
"total_valid_contribution_amount": "10.000",
"num_gr14_contributions": 0,
},
)

def test_contributor_statistics_with_below_threshold_contributions_contributions(
self,
):
existing_contributions = ProtocolContributions.objects.filter(
contributor=self.address
).all()
for contribution in existing_contributions:
contribution.amount = 0.5
contribution.save()

# Edge case: User has made no contributions
response = self.client.get(
reverse("cgrants:allo_contributor_statistics"),
{"address": self.address},
**self.headers,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json(),
{
"num_grants_contribute_to": 0,
"num_rounds_contribute_to": 0,
"total_valid_contribution_amount": 0,
"num_gr14_contributions": 0,
},
)
Expand Down
1 change: 1 addition & 0 deletions api/scorer/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
# "debug_toolbar",
"cgrants",
"django_filters",
"trusta_labs",
]

AUTHENTICATION_BACKENDS = [
Expand Down
7 changes: 7 additions & 0 deletions api/scorer/settings/gitcoin_passport_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@
"CyberProfilePremium": "1.21",
"CyberProfilePaid": "1.21",
"CyberProfileOrgMember": "1.21",
"GrantsStack3Projects": "1.07",
"GrantsStack5Projects": "1.07",
"GrantsStack7Projects": "1.07",
"GrantsStack2Programs": "1.07",
"GrantsStack4Programs": "1.07",
"GrantsStack6Programs": "1.07",
"TrustaLabs": "1.54",
}


Expand Down
1 change: 1 addition & 0 deletions api/scorer/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@
path("social/", include("social_django.urls", namespace="social")),
path("passport-admin/", passport_admin_api.urls),
# path("__debug__/", include("debug_toolbar.urls")),
path("trusta_labs/", include("trusta_labs.urls")),
]
Empty file added api/trusta_labs/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions api/trusta_labs/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib import admin
from trusta_labs.models import TrustaLabsScore


class TrustaLabsScoreAdmin(admin.ModelAdmin):
list_display = ["address", "sybil_risk_score"]
search_fields = ["address"]
search_help_text = "This will perform an exact case insensitive search by 'address'"
show_full_result_count = False


admin.site.register(TrustaLabsScore, TrustaLabsScoreAdmin)
60 changes: 60 additions & 0 deletions api/trusta_labs/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from django.conf import settings
from ninja import Schema
from ninja.security import APIKeyHeader
from ninja_extra import NinjaExtraAPI, status
from ninja_extra.exceptions import APIException
from trusta_labs.models import TrustaLabsScore

api = NinjaExtraAPI(urls_namespace="trusta_labs")


class CgrantsApiKey(APIKeyHeader):
param_name = "AUTHORIZATION"

def authenticate(self, request, key):
if key == settings.CGRANTS_API_TOKEN:
return key


cg_api_key = CgrantsApiKey()


class TrustaLabsScorePayload(Schema):
address: str
score: int


class TrustaLabsScoreResponse(Schema):
address: str
score: int


class TrustaLabsScoreHasNoPayload(APIException):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
default_detail = "There is no payload with this request"


class TrustaLabsScoreHasNoAddress(APIException):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
default_detail = "A Trusta Lab score must be accompanied by an address"


class TrustaLabsScoreHasNoScore(APIException):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
default_detail = "A Trusta Lab request must include a score"


@api.post("/trusta-labs-score", auth=cg_api_key)
def create_trusta_labs_score_db(request, payload: TrustaLabsScorePayload):
if payload == None:
raise TrustaLabsScoreHasNoPayload()

if payload.address == None:
raise TrustaLabsScoreHasNoAddress()

if payload.score == None:
raise TrustaLabsScoreHasNoScore()

TrustaLabsScore.objects.update_or_create(
address=payload.address, sybil_risk_score=payload.score
)
6 changes: 6 additions & 0 deletions api/trusta_labs/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class TrustaLabsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "trusta_labs"
36 changes: 36 additions & 0 deletions api/trusta_labs/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.3 on 2023-08-14 17:52

import account.models
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="TrustaLabsScore",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"address",
account.models.EthAddressField(
db_index=True, max_length=100, null=True
),
),
("sybil_risk_score", models.IntegerField(blank=True, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
),
]
Empty file.
12 changes: 12 additions & 0 deletions api/trusta_labs/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from account.models import EthAddressField
from django.db import models

# WHEN a Trusta Labs API call is made for verification,
# THEN the result of the call, including timestamps, passport addresses, and Trusta Labs scores, should be logged in a dedicated table, separate from the credential issuance.


class TrustaLabsScore(models.Model):
address = EthAddressField(null=True, blank=False, max_length=100, db_index=True)
sybil_risk_score = models.IntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
36 changes: 36 additions & 0 deletions api/trusta_labs/test/test_trusta_labs_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import json
from datetime import datetime, timezone
from typing import cast

from django.conf import settings
from django.test import Client, TestCase
from trusta_labs.models import TrustaLabsScore

mock_trusta_labs_score_body = {
"address": "0x8u3eu3ydh3rydh3irydhu",
"score": 20,
}


class TrustaLabsScoreTestCase(TestCase):
def test_create_trusta_labs_score(self):
self.headers = {"HTTP_AUTHORIZATION": settings.CGRANTS_API_TOKEN}
"""Test that creation of a trusta lab score works and saved correctly"""
client = Client()
trusta_labs_response = client.post(
"/trusta_labs/trusta-labs-score",
json.dumps(mock_trusta_labs_score_body),
content_type="application/json",
**self.headers
)
self.assertEqual(trusta_labs_response.status_code, 200)

# Check that the trusta lab score was created
all_trusta_labs_scores = list(TrustaLabsScore.objects.all())
self.assertEqual(len(all_trusta_labs_scores), 1)
score = all_trusta_labs_scores[0]
self.assertEqual(score.address, mock_trusta_labs_score_body["address"])
self.assertEqual(score.sybil_risk_score, mock_trusta_labs_score_body["score"])

def test_error_creating_trusta_lab_score(self):
pass
7 changes: 7 additions & 0 deletions api/trusta_labs/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path

from .api import api

urlpatterns = [
path("", api.urls),
]