Skip to content

add social profile data indexing #31

New issue

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

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

Already on GitHub? # to your account

Merged
merged 2 commits into from
Jun 6, 2024
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
22 changes: 22 additions & 0 deletions accounts/migrations/0002_account_near_social_profile_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 5.0.4 on 2024-06-05 14:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("accounts", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="account",
name="near_social_profile_data",
field=models.JSONField(
help_text="NEAR social data contained under 'profile' key.",
null=True,
verbose_name="NEAR social profile data",
),
),
]
84 changes: 83 additions & 1 deletion accounts/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import requests
from django import db
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from base.logging import logger


class Account(models.Model):
id = models.CharField(
Expand Down Expand Up @@ -39,5 +43,83 @@ class Account(models.Model):
default=0,
help_text=_("Number of donors."),
)
near_social_profile_data = models.JSONField(
_("NEAR social profile data"),
null=True,
help_text=_("NEAR social data contained under 'profile' key."),
)

def fetch_near_social_profile_data(self, should_save=True):
# Fetch social profile data from NEAR blockchain
try:
url = f"{settings.FASTNEAR_RPC_URL}/account/{settings.NEAR_SOCIAL_CONTRACT_ADDRESS}/view/get"
keys_value = f'["{self.id}/profile/**"]'
params = {"keys.json": keys_value}
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
if self.id in data and "profile" in data[self.id]:
profile_data = data[self.id][
"profile"
] # TODO: validate/sanitize profile data?
# fetch NFT URLs if applicable
for image_type in ["image", "backgroundImage"]:
if (
image_type in profile_data
and "nft" in profile_data[image_type]
and "contractId" in profile_data[image_type]["nft"]
and "tokenId" in profile_data[image_type]["nft"]
):
contract_id = profile_data[image_type]["nft"]["contractId"]
token_id = profile_data[image_type]["nft"]["tokenId"]
# get base_uri
url = f"https://rpc.web4.near.page/account/{contract_id}/view/nft_metadata"
response = requests.get(url)
if response.status_code == 200:
metadata = response.json()
if "base_uri" in metadata:
base_uri = metadata["base_uri"]
# store baseUri in profile_data
profile_data[image_type]["nft"][
"baseUri"
] = base_uri
else:
logger.error(
f"Request for NFT metadata failed ({response.status_code}) with message: {response.text}"
)
# get token metadata
url = f"https://rpc.web4.near.page/account/{contract_id}/view/nft_token"
json_data = {"token_id": token_id}
response = requests.post(
url, json=json_data
) # using a POST request here so that token_id is not coerced into an integer on fastnear's side, causing a contract view error
if response.status_code == 200:
token_metadata = response.json()
if (
"metadata" in token_metadata
and "media" in token_metadata["metadata"]
):
# store media in profile_data
profile_data[image_type]["nft"]["media"] = (
token_metadata["metadata"]["media"]
)
else:
logger.error(
f"Request for NFT metadata failed ({response.status_code}) with message: {response.text}"
)
self.near_social_profile_data = profile_data
if should_save:
self.save()
else:
logger.error(
f"Request for NEAR Social profile data failed ({response.status_code}) with message: {response.text}"
)
except Exception as e:
logger.error(f"Error fetching NEAR social profile data: {e}")

# add Meta, properties & methods as necessary
def save(self, *args, **kwargs):
if self._state.adding: # If the account is being created (not updated)
self.fetch_near_social_profile_data(
False # don't save yet as we want to avoid infinite loop
)
super().save(*args, **kwargs)
1 change: 1 addition & 0 deletions accounts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ class Meta:
"total_donations_out_usd",
"total_matching_pool_allocations_usd",
"donors_count",
"near_social_profile_data",
]
10 changes: 10 additions & 0 deletions base/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@

POTLOCK_TLA = "potlock.testnet" if ENVIRONMENT == "testnet" else "potlock.near"

NEAR_SOCIAL_CONTRACT_ADDRESS = (
"v1.social08.testnet" if ENVIRONMENT == "testnet" else "social.near"
)

FASTNEAR_RPC_URL = (
"https://rpc.web4.testnet.page"
if ENVIRONMENT == "testnet"
else "https://rpc.web4.near.page"
)

BLOCK_SAVE_HEIGHT = os.environ.get("BLOCK_SAVE_HEIGHT")

COINGECKO_URL = (
Expand Down
22 changes: 16 additions & 6 deletions indexer_app/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
handle_pot_application,
handle_pot_application_status_change,
handle_set_payouts,
handle_social_profile_update,
handle_transfer_payout,
)

Expand All @@ -46,19 +47,21 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess

for shard in streamer_message.shards:
for receipt_execution_outcome in shard.receipt_execution_outcomes:
# we only want to proceed if it's a potlock tx and it succeeded.... (unreadable if statement?)
lists_contract = "lists." + settings.POTLOCK_TLA
if not receipt_execution_outcome.receipt.receiver_id.endswith(
settings.POTLOCK_TLA
) or (
# we only want to proceed if the tx succeeded
if (
"SuccessReceiptId"
not in receipt_execution_outcome.execution_outcome.outcome.status
and "SuccessValue"
not in receipt_execution_outcome.execution_outcome.outcome.status
):
continue
receiver_id = receipt_execution_outcome.receipt.receiver_id
if (
receiver_id != settings.NEAR_SOCIAL_CONTRACT_ADDRESS
and not receiver_id.endswith(settings.POTLOCK_TLA)
):
continue
# 1. HANDLE LOGS

log_data = []

for log_index, log in enumerate(
Expand Down Expand Up @@ -89,6 +92,7 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess
# # consider logging failures to logging service; for now, just skip
# print("here we are...")
# continue
lists_contract = "lists." + settings.POTLOCK_TLA

for index, action in enumerate(
receipt_execution_outcome.receipt.receipt["Action"]["actions"]
Expand Down Expand Up @@ -125,6 +129,12 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess
args_dict = {}

match method_name:
case "set": # handle near social profile data updates
if receiver_id == settings.NEAR_SOCIAL_CONTRACT_ADDRESS:
logger.info(f"setting profile data: {args_dict}")
await handle_social_profile_update(
args_dict, receiver_id, signer_id
)
case "new":
if match_pot_factory_pattern(receipt.receiver_id):
logger.info(f"matched for factory pattern: {args_dict}")
Expand Down
18 changes: 17 additions & 1 deletion indexer_app/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import base64
import decimal
import json
from datetime import datetime

Expand Down Expand Up @@ -29,6 +28,23 @@
# GECKO_URL = "https://api.coingecko.com/api/v3" # TODO: move to settings


async def handle_social_profile_update(args_dict, receiver_id, signer_id):
logger.info(f"handling social profile update for {signer_id}")
if (
"data" in args_dict
and signer_id in args_dict["data"]
and "profile" in args_dict["data"][signer_id]
):
try:
# only proceed if this account already exists in db
account = await Account.objects.filter(id=signer_id).first()
if account:
logger.info(f"updating social profile for {signer_id}")
account.fetch_near_social_profile_data()
except Exception as e:
logger.error(f"Error in handle_social_profile_update: {e}")


async def handle_new_pot(
data: dict,
receiverId: str,
Expand Down
17 changes: 17 additions & 0 deletions lists/migrations/0003_alter_listupvote_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.4 on 2024-06-05 14:02

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("lists", "0002_alter_listupvote_options_and_more"),
]

operations = [
migrations.AlterModelOptions(
name="listupvote",
options={"verbose_name_plural": "List Upvotes"},
),
]
33 changes: 33 additions & 0 deletions pots/migrations/0003_alter_potapplication_options_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.0.4 on 2024-06-05 14:02

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("pots", "0002_alter_potapplication_options_and_more"),
]

operations = [
migrations.AlterModelOptions(
name="potapplication",
options={"verbose_name_plural": "Pot Applications"},
),
migrations.AlterModelOptions(
name="potapplicationreview",
options={"verbose_name_plural": "Pot Application Reviews"},
),
migrations.AlterModelOptions(
name="potfactory",
options={"verbose_name_plural": "Pot Factories"},
),
migrations.AlterModelOptions(
name="potpayoutchallenge",
options={"verbose_name_plural": "Payout Challenges"},
),
migrations.AlterModelOptions(
name="potpayoutchallengeadminresponse",
options={"verbose_name_plural": "Payout Challenge Responses"},
),
]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ djangorestframework = "^3.15.1"
django-cachalot = "^2.6.2"
django-redis = "^5.4.0"
gunicorn = "^22.0.0"
sentry-sdk = {extras = ["django"], version = "^1.45.0"}
sentry-sdk = { extras = ["django"], version = "^1.45.0" }
watchtower = "^3.1.0"
django-cors-headers = "^4.3.1"
drf-spectacular = "^0.27.2"
Expand Down