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): generic community scorer creation #190

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
70 changes: 65 additions & 5 deletions api/account/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ninja_jwt.exceptions import InvalidToken
from ninja_jwt.schema import RefreshToken
from ninja_schema import Schema
from registry.api import ApiKey
from scorer_weighted.models import BinaryWeightedScorer, WeightedScorer
from siwe import SiweMessage, siwe

Expand Down Expand Up @@ -152,8 +153,7 @@ class Config:
model_fields = ["name", "description", "id", "created_at", "use_case"]


@api.post("/verify", response=TokenObtainPairOutSchema)
def submit_signed_challenge(request, payload: SiweVerifySubmit):
def verify_siwe_payload(payload: SiweVerifySubmit):
payload.message["chain_id"] = payload.message["chainId"]
payload.message["issued_at"] = payload.message["issuedAt"]

Expand All @@ -163,17 +163,32 @@ def submit_signed_challenge(request, payload: SiweVerifySubmit):
try:
message: SiweMessage = SiweMessage(payload.message)
verifyParams = {
"signature": payload.signature,
"signatures": payload.signature,
# See note in /nonce function above
# "nonce": request.session["nonce"],
}

message.verify(**verifyParams)
except siwe.DomainMismatch:

except Exception as e:
raise e


def raise_siwe_exception(e):
if isinstance(e, siwe.DomainMismatch):
raise InvalidDomainException()
except siwe.VerificationError:
else:
raise FailedVerificationException()


@api.post("/verify", response=TokenObtainPairOutSchema)
def submit_signed_challenge(request, payload: SiweVerifySubmit):
try:
verify_siwe_payload(payload)
except Exception as e:
# needs to be raised here to be caught by the middleware
raise_siwe_exception(e)

address_lower = payload.message["address"]

try:
Expand Down Expand Up @@ -508,3 +523,48 @@ def update_community_scorers(request, community_id, payload: ScorerId):
except Community.DoesNotExist:
raise UnauthorizedException()
return {"ok": True}


class GenericCommunitiesPayload(Schema):
name: str
siwe_submit: SiweVerifySubmit
description: Optional[str] = "Programmatically created by Allo"
use_case: Optional[str] = "Sybil Prevention"


# Should we try to update naming Scorer vs Community?
@api.post("/generic-community", auth=ApiKey())
def create_generic_community(request, payload: GenericCommunitiesPayload):
try:
verify_siwe_payload(payload.siwe_submit)
except Exception as e:
# needs to be raised here to be caught by the middleware
raise_siwe_exception(e)

try:
account = request.user.account

if not account.privileged:
raise UnauthorizedException()

if Community.objects.filter(name=payload.name, account=account).exists():
raise CommunityExistsException()

# Create generic community
community = Community.objects.create(
account=account,
name=payload.name,
description=payload.description,
use_case=payload.use_case,
)

except Account.DoesNotExist:
raise UnauthorizedException()

return {
"ok": True,
"id": community.pk,
"name": community.name,
"description": community.description,
"use_case": community.use_case,
}
17 changes: 17 additions & 0 deletions api/account/migrations/0013_account_privileged.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2023-03-27 23:03

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("account", "0012_alter_community_rule"),
]

operations = [
migrations.AddField(
model_name="account",
name="privileged",
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions api/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class Account(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="account"
)
privileged = models.BooleanField(default=False)

def __str__(self):
return f"{self.address} - {self.user}"
Expand Down
38 changes: 38 additions & 0 deletions api/account/test/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,45 @@
from datetime import datetime

import pytest
from account.models import Nonce
from django.conf import settings

# pylint: disable=unused-import
from scorer.test.conftest import (
access_token,
scorer_account,
scorer_community,
scorer_user,
)
from web3 import Web3

my_mnemonic = settings.TEST_MNEMONIC


@pytest.fixture
def nonce():
Nonce.create_nonce().nonce


@pytest.fixture
def web3_account():
web3 = Web3()
web3.eth.account.enable_unaudited_hdwallet_features()
account = web3.eth.account.from_mnemonic(
my_mnemonic, account_path="m/44'/60'/0'/0/0"
)
return account


@pytest.fixture
def siwe_data(nonce):
return {
"domain": "localhost:3000",
"address": web3_account.address,
"statement": f"Welcome to Gitcoin Passport Scorer! This request will not trigger a blockchain transaction or cost any gas fees. Your authentication status will reset in 24 hours. Wallet Address: ${account.address}. Nonce: ${nonce}",
"uri": "http://localhost/",
"version": "1",
"chainId": "1",
"nonce": nonce.nonce,
"issuedAt": datetime.utcnow().isoformat(),
}
128 changes: 59 additions & 69 deletions api/account/test/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from copy import deepcopy
from datetime import datetime

import pytest
from django.conf import settings
from django.test import Client, TestCase
from eth_account.messages import encode_defunct
Expand All @@ -25,29 +26,17 @@

c = Client()

pytestmark = pytest.mark.django_db

class AccountTestCase(TestCase):
def setUp(self):
pass

def test_create_account_with_SIWE(self):
class AccountTestCase:
def test_create_account_with_SIWE(self, siwe_data, web3_account):
"""Test creation of an account wit SIWE"""
response = c.get("/account/nonce")
self.assertEqual(200, response.status_code)
breakpoint()
assert 200 == response.status_code

data = response.json()
nonce = data["nonce"]

siwe_data = {
"domain": "localhost:3000",
"address": account.address,
"statement": f"Welcome to Gitcoin Passport Scorer! This request will not trigger a blockchain transaction or cost any gas fees. Your authentication status will reset in 24 hours. Wallet Address: ${account.address}. Nonce: ${nonce}",
"uri": "http://localhost/",
"version": "1",
"chainId": "1",
"nonce": data["nonce"],
"issuedAt": datetime.utcnow().isoformat(),
}

siwe_data_pay = deepcopy(siwe_data)
siwe_data_pay["chain_id"] = siwe_data_pay["chainId"]
Expand All @@ -56,7 +45,7 @@ def test_create_account_with_SIWE(self):
siwe = SiweMessage(siwe_data_pay)
data_to_sign = siwe.prepare_message()

private_key = account.key
private_key = web3_account.key
signed_message = w3.eth.account.sign_message(
encode_defunct(text=data_to_sign), private_key=private_key
)
Expand All @@ -73,55 +62,56 @@ def test_create_account_with_SIWE(self):
),
content_type="application/json",
)
self.assertEqual(200, response.status_code)
data = response.json()
# Refresh/access JWT created by django
self.assertTrue("refresh" in data)
self.assertTrue("access" in data)

def test_create_account_with_www_domain(self):
response = c.get("/account/nonce")
self.assertEqual(200, response.status_code)

data = response.json()

siwe_data = {
"domain": "www.localhost:3000",
"address": account.address,
"statement": "Sign in with Ethereum to the app.",
"uri": "http://www.localhost/",
"version": "1",
"chainId": "1",
"nonce": data["nonce"],
"issuedAt": datetime.utcnow().isoformat(),
}

siwe_data_pay = deepcopy(siwe_data)
siwe_data_pay["chain_id"] = siwe_data_pay["chainId"]
siwe_data_pay["issued_at"] = siwe_data_pay["issuedAt"]

siwe = SiweMessage(siwe_data_pay)
data_to_sign = siwe.prepare_message()

private_key = account.key
signed_message = w3.eth.account.sign_message(
encode_defunct(text=data_to_sign), private_key=private_key
)

response = c.post(
"/account/verify",
json.dumps(
{
"message": siwe_data,
"signature": binascii.hexlify(signed_message.signature).decode(
"utf-8"
),
}
),
content_type="application/json",
)
self.assertEqual(200, response.status_code)
data = response.json()
# Refresh/access JWT created by django
self.assertTrue("refresh" in data)
self.assertTrue("access" in data)
# self.assertEqual(200, response.status_code)
# data = response.json()
# # Refresh/access JWT created by django
# self.assertTrue("refresh" in data)
# self.assertTrue("access" in data)

# def test_create_account_with_www_domain(self):
# response = c.get("/account/nonce")
# self.assertEqual(200, response.status_code)

# data = response.json()

# siwe_data = {
# "domain": "www.localhost:3000",
# "address": account.address,
# "statement": "Sign in with Ethereum to the app.",
# "uri": "http://www.localhost/",
# "version": "1",
# "chainId": "1",
# "nonce": data["nonce"],
# "issuedAt": datetime.utcnow().isoformat(),
# }

# siwe_data_pay = deepcopy(siwe_data)
# siwe_data_pay["chain_id"] = siwe_data_pay["chainId"]
# siwe_data_pay["issued_at"] = siwe_data_pay["issuedAt"]

# siwe = SiweMessage(siwe_data_pay)
# data_to_sign = siwe.prepare_message()

# private_key = account.key
# signed_message = w3.eth.account.sign_message(
# encode_defunct(text=data_to_sign), private_key=private_key
# )

# response = c.post(
# "/account/verify",
# json.dumps(
# {
# "message": siwe_data,
# "signature": binascii.hexlify(signed_message.signature).decode(
# "utf-8"
# ),
# }
# ),
# content_type="application/json",
# )
# self.assertEqual(200, response.status_code)
# data = response.json()
# # Refresh/access JWT created by django
# self.assertTrue("refresh" in data)
# self.assertTrue("access" in data)