added blockchain operations, transaction history, minor optimisations

This commit is contained in:
Alexander Karpov 2022-10-09 02:24:31 +03:00
parent 12f81dbf3f
commit 79587c322d
15 changed files with 209 additions and 166 deletions

View File

3
app/blockchain/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
app/blockchain/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BlockchainConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blockchain'

View File

@ -0,0 +1,15 @@
# Generated by Django 4.0.8 on 2022-10-08 17:58
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = []

View File

52
app/blockchain/models.py Normal file
View File

@ -0,0 +1,52 @@
from django.core.validators import MinValueValidator
from django.db import models
# Create your models here.
from users.models import User
class Transaction(models.Model):
user_from = models.ForeignKey(
User, related_name="transactions_from", on_delete=models.CASCADE
)
user_to = models.ForeignKey(
User, related_name="transactions_to", on_delete=models.CASCADE
)
amount = models.IntegerField(validators=[MinValueValidator(1)])
created = models.DateTimeField(auto_now_add=True)
hash = models.CharField(max_length=256, unique=True)
def __str__(self):
return f"transaction from {self.user_from} to {self.user_to}"
class AdminTransaction(models.Model):
class TransactionType(models.TextChoices):
FROM = "PAYMENT", "payment"
TO = "SALARY", "salary"
type = models.CharField(max_length=7, choices=TransactionType.choices)
amount = models.IntegerField(validators=[MinValueValidator(1)])
user = models.ForeignKey(
User, related_name="admin_transactions", on_delete=models.CASCADE
)
created = models.DateTimeField(auto_now_add=True)
hash = models.CharField(max_length=256, unique=True)
@property
def user_from(self):
return "system" if self.type == self.TransactionType.TO else self.user.username
@property
def user_to(self):
return (
"system" if self.type == self.TransactionType.FROM else self.user.username
)
def __str__(self):
return str(self.amount)

View File

@ -0,0 +1,38 @@
from django.conf import settings
from blockchain.models import Transaction, AdminTransaction
from users.models import User
from utils.blockchain import transfer_rubbles
def list_user_transactions(user: User) -> list:
transaction = []
qs = (
user.transactions_to.all()
| user.transactions_from.all()
| user.admin_transactions.all()
)
qs.order_by("created")
username = user.username
for el in qs: # type: Transaction
transaction.append(
{
"type": "addition" if el.user_from.username == username else "transfer",
"user_from": el.user_from.username,
"user_to": el.user_to.username,
"amount": el.amount,
}
)
return transaction
def transact_from_admin(user: User, amount: int):
priv_key = settings.MAIN_WALLET
t = transfer_rubbles(priv_key, user.wallet_public_key, amount)
AdminTransaction.objects.create(
type="SALARY", amount=amount, user=user, hash=t.transaction_hash
)
user.money += amount
user.save(update_fields=["money"])

40
app/blockchain/signals.py Normal file
View File

@ -0,0 +1,40 @@
from django.db.models.signals import pre_save
from django.dispatch import receiver
from rest_framework.exceptions import ValidationError
from blockchain.models import Transaction
from utils.blockchain import get_balance, transfer_rubbles
@receiver(pre_save, sender=Transaction)
def validate_create_transaction(sender, instance: Transaction, **kwargs):
# validate transaction
if instance.user_from == instance.user_to:
raise ValidationError("Cannot transfer to yourself")
if instance.amount == 0:
raise ValidationError("Cannot transfer 0 money")
# Potential Race condition, use transaction with atomics in prod
user_from_money = int(get_balance(instance.user_from.wallet_public_key).coins)
if instance.user_from.money != user_from_money:
instance.user_from.money = user_from_money
instance.user_from.save(update_fields=["money"])
if user_from_money - instance.amount <= 0:
raise ValidationError(
f"{instance.user_from.username} doesn't have enough money"
)
transaction = transfer_rubbles(
instance.user_from.wallet_private_key,
instance.user_to.wallet_public_key,
amount=instance.amount,
)
instance.user_from.money -= instance.amount
instance.user_from.save(update_fields=["money"])
instance.user_to.money += instance.amount
instance.user_to.save(update_fields=["money"])
instance.hash = transaction.transaction_hash

View File

@ -9,37 +9,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [ operations = []
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('slug', models.SlugField(max_length=8)),
('about', models.TextField(blank=True)),
('starts', models.DateTimeField()),
('image', models.ImageField(blank=True, upload_to='uploads/')),
('image_cropped', models.ImageField(blank=True, upload_to='cropped/')),
('planning', models.IntegerField(default=0)),
('attended', models.IntegerField(default=0)),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events_created', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-starts'],
},
),
migrations.CreateModel(
name='EventAttendance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=128, unique=True)),
('attended', models.BooleanField(default=False)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='people', to='events.event')),
('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,32 @@
[
{
"model": "users.department",
"pk": 1,
"fields": {
"name": "Backend"
}
},
{
"model": "users.department",
"pk": 2,
"fields": {
"name": "Frontend"
}
},
{
"model": "users.stream",
"pk": 1,
"fields": {
"name": "Colloring buttons",
"department": 2
}
},
{
"model": "users.command",
"pk": 1,
"fields": {
"name": "Colloring main page button go get money",
"stream": 1
}
}
]

View File

@ -9,22 +9,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [ operations = []
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('description', models.TextField()),
('image', models.ImageField(upload_to='uploads/')),
('image_cropped', models.ImageField(blank=True, upload_to='cropped/')),
('nft', models.CharField(blank=True, max_length=500)),
('price', models.IntegerField()),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,7 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from ..services import create_season from ..services import create_season
from users.models import User, Clan from users.models import User, Department, Stream, Command, Clan
from users.models import User, Department, Stream, Command
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
@ -19,12 +18,14 @@ class UserSerializer(serializers.ModelSerializer):
"command", "command",
"department", "department",
"clan_name", "clan_name",
"money",
] ]
extra_kwargs = { extra_kwargs = {
"password": {"write_only": True}, "password": {"write_only": True},
"wallet_public_key": {"read_only": True}, "wallet_public_key": {"read_only": True},
"clan_name": {"read_only": True}, "clan_name": {"read_only": True},
"department": {"read_only": True}, "department": {"read_only": True},
"money": {"read_only": True},
} }
def create(self, validated_data): def create(self, validated_data):
@ -41,6 +42,9 @@ class CreateSeasonSerializer(serializers.Serializer):
create_season() create_season()
return {"created": True} return {"created": True}
def update(self, instance, validated_data):
pass
class CommandSerializer(serializers.ModelSerializer): class CommandSerializer(serializers.ModelSerializer):
workers = UserSerializer(many=True) workers = UserSerializer(many=True)

View File

@ -10,110 +10,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [ operations = []
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
("about", models.TextField(blank=True)),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
"ordering": ["-id"],
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -22,7 +22,9 @@ class User(AbstractUser):
type = models.CharField( type = models.CharField(
max_length=6, choices=WorkerType.choices, default=WorkerType.WORKER max_length=6, choices=WorkerType.choices, default=WorkerType.WORKER
) )
clan = models.ForeignKey("users.Clan", related_name="users", on_delete=models.SET_NULL, null=True) clan = models.ForeignKey(
"users.Clan", related_name="users", on_delete=models.SET_NULL, null=True
)
command = models.ForeignKey( command = models.ForeignKey(
"users.Command", related_name="workers", on_delete=models.CASCADE "users.Command", related_name="workers", on_delete=models.CASCADE
) )
@ -30,6 +32,8 @@ class User(AbstractUser):
respect = models.IntegerField(default=0, validators=[MinValueValidator(0)]) respect = models.IntegerField(default=0, validators=[MinValueValidator(0)])
wallet_private_key = models.CharField(max_length=96, unique=True) wallet_private_key = models.CharField(max_length=96, unique=True)
wallet_public_key = models.CharField(max_length=96, unique=True) wallet_public_key = models.CharField(max_length=96, unique=True)
money = models.IntegerField(default=0, validators=[MinValueValidator(0)])
telegram = models.CharField(max_length=100, unique=True) telegram = models.CharField(max_length=100, unique=True)
def __str__(self): def __str__(self):

View File

@ -2,8 +2,8 @@ from random import shuffle
from django.conf import settings from django.conf import settings
from blockchain.services import transact_from_admin
from .models import Clan, User from .models import Clan, User
from utils.blockchain import transfer_rubbles
import requests as r import requests as r
import json import json
@ -16,13 +16,13 @@ def end_season():
mx_value = sum(map(lambda user: user.respect, clan.users.all())) mx_value = sum(map(lambda user: user.respect, clan.users.all()))
mx_clan = clan mx_clan = clan
for user in mx_clan.users.all(): for user in mx_clan.users.all():
transfer_rubbles( transact_from_admin(user, 100)
settings.MAIN_WALLET,
user.wallet_public_key,
100,
)
Clan.objects.all().delete() Clan.objects.all().delete()
for user in User.objects.filter(type=User.WorkerType.WORKER):
if user.salary > 0:
transact_from_admin(user, user.salary)
def create_chat(clan: Clan): def create_chat(clan: Clan):
user_list = list(map(lambda user: user.telegram, clan.users.all())) user_list = list(map(lambda user: user.telegram, clan.users.all()))