diff --git a/app/dashboard/admin.py b/app/dashboard/admin.py index 8520b8961ed..0a61d3ed8ac 100644 --- a/app/dashboard/admin.py +++ b/app/dashboard/admin.py @@ -30,8 +30,8 @@ BountySyncRequest, CoinRedemption, CoinRedemptionRequest, Coupon, Earning, FeedbackEntry, FundRequest, HackathonEvent, HackathonProject, HackathonRegistration, HackathonSponsor, HackathonWorkshop, Interest, Investigation, LabsResearch, ObjectView, Option, Poll, PollMedia, PortfolioItem, Profile, ProfileVerification, - ProfileView, Question, SearchHistory, Sponsor, Tip, TipPayout, TokenApproval, TribeMember, TribesSubscription, - UserAction, UserVerificationModel, + ProfileView, Question, SearchHistory, Sponsor, Tip, TipPayout, TokenApproval, TransactionHistory, TribeMember, + TribesSubscription, UserAction, UserVerificationModel, ) @@ -55,6 +55,15 @@ class GeneralAdmin(admin.ModelAdmin): list_display = ['created_on', '__str__'] +class TransactionHistoryAdmin(admin.ModelAdmin): + ordering = ['-id'] + list_display = ['created_on', 'status', '__str__'] + raw_id_fields = ['earning'] + search_fields = [ + 'payload', 'txid', 'status' + ] + + class ObjectViewAdmin(admin.ModelAdmin): ordering = ['-id'] list_display = ['created_on', '__str__'] @@ -606,6 +615,7 @@ class ProfileVerificationAdmin(admin.ModelAdmin): admin.site.register(LabsResearch) admin.site.register(Investigation, InvestigationAdmin) admin.site.register(UserVerificationModel, VerificationAdmin) +admin.site.register(TransactionHistory, TransactionHistoryAdmin) admin.site.register(Coupon, CouponAdmin) admin.site.register(TribeMember, TribeMemberAdmin) admin.site.register(TribesSubscription, TribesSubscriptionAdmin) diff --git a/app/dashboard/migrations/0154_transactionhistory.py b/app/dashboard/migrations/0154_transactionhistory.py new file mode 100644 index 00000000000..6cff8b65ea7 --- /dev/null +++ b/app/dashboard/migrations/0154_transactionhistory.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.4 on 2020-10-11 22:12 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0153_hackathonevent_use_circle'), + ] + + operations = [ + migrations.CreateModel( + name='TransactionHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('txid', models.CharField(default='', max_length=255)), + ('status', models.CharField(db_index=True, default='', max_length=50)), + ('payload', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, null=True)), + ('network', models.CharField(blank=True, db_index=True, max_length=255)), + ('captured_at', models.DateTimeField()), + ('earning', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='history', to='dashboard.Earning')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index b3bd88eaa37..0e2ad75ce44 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -5713,3 +5713,16 @@ class HackathonWorkshop(SuperModel): ) url = models.URLField(help_text='Blog link, calendar link, or other.') visible = models.BooleanField(help_text=_('Can this HackathonWorkshop be seen on /hackathons ?'), default=True) + + +class TransactionHistory(SuperModel): + + earning = models.ForeignKey('dashboard.Earning', related_name='history', on_delete=models.CASCADE, db_index=True, null=True) + txid = models.CharField(max_length=255, default='') + status = models.CharField(max_length=50, default='', db_index=True) + payload = JSONField(default=dict, blank=True, null=True) + network = models.CharField(max_length=255, blank=True, db_index=True) + captured_at = models.DateTimeField() + + def __str__(self): + return f"{self.status} <> {self.earning.pk} at {self.captured_at}" diff --git a/app/dashboard/utils.py b/app/dashboard/utils.py index 522a9aa20a3..76182e687b9 100644 --- a/app/dashboard/utils.py +++ b/app/dashboard/utils.py @@ -806,10 +806,17 @@ def profile_helper(handle, suppress_profile_hidden_exception=False, current_user return profile + def is_valid_eth_address(eth_address): return (bool(re.match(r"^0x[a-zA-Z0-9]{40}$", eth_address)) or eth_address == "0x0") + def get_tx_status(txid, network, created_on): + status, timestamp, tx = get_tx_status_and_details(txid, network, created_on) + return status, timestamp + + +def get_tx_status_and_details(txid, network, created_on): from django.utils import timezone from dashboard.utils import get_web3 import pytz @@ -817,6 +824,7 @@ def get_tx_status(txid, network, created_on): DROPPED_DAYS = 4 # get status + tx = {} status = None if txid == 'override': return 'success', None #overridden by admin @@ -853,7 +861,7 @@ def get_tx_status(txid, network, created_on): timestamp = timezone.datetime.fromtimestamp(timestamp).replace(tzinfo=pytz.UTC) except: pass - return status, timestamp + return status, timestamp, tx def is_blocked(handle): diff --git a/app/economy/models.py b/app/economy/models.py index 75eb36ad49f..4ecd0d84a1f 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -41,14 +41,20 @@ import pytz from app.services import RedisService +from hexbytes import HexBytes +from web3.utils.datastructures import AttributeDict class EncodeAnything(DjangoJSONEncoder): def default(self, obj): + if isinstance(obj, AttributeDict): + return dict(obj) if isinstance(obj, Promise): return force_text(obj) elif isinstance(obj, FieldFile): return bool(obj) + elif isinstance(obj, HexBytes): + return str(obj) elif isinstance(obj, SuperModel): return (obj.to_standard_dict()) elif isinstance(obj, models.Model): diff --git a/app/kudos/management/commands/pull_tx_status.py b/app/kudos/management/commands/pull_tx_status.py new file mode 100644 index 00000000000..c8cf9c3090c --- /dev/null +++ b/app/kudos/management/commands/pull_tx_status.py @@ -0,0 +1,53 @@ +"""Define the mint all kudos management command. + +Copyright (C) 2020 Gitcoin Core + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +""" + +import json + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from dashboard.models import Earning, TransactionHistory +from dashboard.utils import get_tx_status_and_details +from economy.models import EncodeAnything + + +class Command(BaseCommand): + + help = 'pulls tx statuses and stores them in the DB to be queried later' + + def handle(self, *args, **options): + earnings = Earning.objects.filter(history__pk__isnull=True).order_by('-pk') + for earning in earnings: + if earning.history.count(): + continue + txid = earning.txid + network = earning.network + created_on = earning.created_on + status, timestamp, tx = get_tx_status_and_details(txid, network, created_on) + print(earning.pk, status) + if status not in ['unknown', 'pending']: + tx = tx if tx else {} + TransactionHistory.objects.create( + earning=earning, + status=status, + payload=json.loads(json.dumps(dict(tx), cls=EncodeAnything)), + network=network, + txid=txid, + captured_at=timezone.now(), + ) diff --git a/scripts/crontab b/scripts/crontab index e285ca502a6..a6cd3e784cb 100644 --- a/scripts/crontab +++ b/scripts/crontab @@ -50,6 +50,7 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/us */8 * * * * cd gitcoin/coin; bash scripts/run_management_command.bash send_tips_for_bounty_fulfiller >> /var/log/gitcoin/send_tips_for_bounty_fulfiller.log 2>&1 35 1 * * * cd gitcoin/coin; bash scripts/run_management_command.bash update_popularity >> /var/log/gitcoin/update_popularity.log 2>&1 1 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash update_tx_status >> /var/log/gitcoin/update_tx_status.log 2>&1 +15 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash pull_tx_status >> /var/log/gitcoin/pull_tx_status.log 2>&1 * * * * * cd gitcoin/coin; sleep 10; bash scripts/run_management_command_if_not_already_running.bash recalculate_profiles >> /var/log/gitcoin/recalculate_profiles.log 2>&1 1 3 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash pull_compliance_list >> /var/log/gitcoin/pull_compliance_list.log 2>&1 25 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash update_search_index update >> /var/log/gitcoin/update_search_index.log 2>&1