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

Add model for locking events #12

Merged
merged 14 commits into from
Feb 28, 2024
2 changes: 1 addition & 1 deletion config/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = True
DEBUG = False
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env(
"DJANGO_SECRET_KEY",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ hexbytes==0.3.1
packaging>=21
psycopg2==2.9.9
requests==2.31.0
safe-eth-py[django]==6.0.0b16
safe-eth-py[django]==6.0.0b17
web3==6.15.1
109 changes: 109 additions & 0 deletions safe_locking_service/locking_events/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Generated by Django 4.2.10 on 2024-02-27 17:41

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

import gnosis.eth.django.models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="EthereumTx",
fields=[
(
"tx_hash",
gnosis.eth.django.models.Keccak256Field(
primary_key=True, serialize=False
),
),
("block_hash", gnosis.eth.django.models.Keccak256Field()),
("block_number", models.PositiveIntegerField()),
("block_timestamp", models.DateTimeField()),
],
),
migrations.CreateModel(
name="UnlockEvent",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("timestamp", models.DateTimeField()),
("log_index", models.PositiveIntegerField()),
("holder", gnosis.eth.django.models.EthereumAddressV2Field()),
("amount", gnosis.eth.django.models.Uint96Field()),
("unlock_index", models.PositiveIntegerField()),
(
"ethereum_tx",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="locking_events.ethereumtx",
),
),
],
),
migrations.CreateModel(
name="WithdrawnEvent",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("timestamp", models.DateTimeField()),
("log_index", models.PositiveIntegerField()),
("holder", gnosis.eth.django.models.EthereumAddressV2Field()),
("amount", gnosis.eth.django.models.Uint96Field()),
(
"ethereum_tx",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="locking_events.ethereumtx",
),
),
(
"unlock_index",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="locking_events.unlockevent",
),
),
],
),
migrations.CreateModel(
name="LockEvent",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("timestamp", models.DateTimeField()),
("log_index", models.PositiveIntegerField()),
("holder", gnosis.eth.django.models.EthereumAddressV2Field()),
("amount", gnosis.eth.django.models.Uint96Field()),
(
"ethereum_tx",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="locking_events.ethereumtx",
),
),
],
options={
"abstract": False,
},
),
migrations.AddConstraint(
model_name="withdrawnevent",
constraint=models.UniqueConstraint(
fields=("holder", "unlock_index"), name="unique_withdrawn_event_index"
),
),
migrations.AddConstraint(
model_name="unlockevent",
constraint=models.UniqueConstraint(
fields=("holder", "unlock_index"), name="unique_unlock_event_index"
),
),
migrations.AddConstraint(
model_name="lockevent",
constraint=models.UniqueConstraint(
fields=("ethereum_tx", "log_index"), name="unique_ethereum_tx_log_index"
),
),
]
86 changes: 84 additions & 2 deletions safe_locking_service/locking_events/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,85 @@
# from django.db import models
from django.db import models

# Create your models here.
from gnosis.eth.django.models import EthereumAddressV2Field, Keccak256Field, Uint96Field


class EthereumTx(models.Model):
tx_hash = Keccak256Field(primary_key=True)
block_hash = Keccak256Field()
block_number = models.PositiveIntegerField()
block_timestamp = models.DateTimeField()

def __str__(self):
return f"Transaction hash {self.tx_hash}"


class CommonEvent(models.Model):
"""
Abstract model that defines generic fields of a locking event. (Abstract model doesn't create tables)
The timestamp is stored also in this model to improve the query performance.
"""

id = models.AutoField(primary_key=True)
timestamp = models.DateTimeField()
ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE)
log_index = models.PositiveIntegerField()
holder = EthereumAddressV2Field()
amount = Uint96Field()

class Meta:
abstract = True
constraints = [
models.UniqueConstraint(
fields=["ethereum_tx", "log_index"], name="unique_ethereum_tx_log_index"
)
]

def __str__(self):
return f"timestamp={self.timestamp} tx-hash={self.ethereum_tx_id} log_index={self.log_index} holder={self.holder} amount={self.amount}"


class LockEvent(CommonEvent):
"""
Model to store event Locked(address indexed holder, uint96 amount)
"""

pass

def __str__(self):
return "LockEvent: " + super().__str__()


class UnlockEvent(CommonEvent):
"""
Model to store event Unlocked(address indexed holder, uint32 indexed index, uint96 amount)
"""

unlock_index = models.PositiveIntegerField()

class Meta:
constraints = [
models.UniqueConstraint(
fields=["holder", "unlock_index"], name="unique_unlock_event_index"
)
]

def __str__(self):
return "UnlockEvent: " + super().__str__()


class WithdrawnEvent(CommonEvent):
"""
Model to store event Withdrawn(address indexed holder, uint32 indexed index, uint96 amount)
"""

unlock_index = models.ForeignKey(UnlockEvent, on_delete=models.CASCADE)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["holder", "unlock_index"], name="unique_withdrawn_event_index"
)
]

def __str__(self):
return "WithdrawnEvent: " + super().__str__()
58 changes: 58 additions & 0 deletions safe_locking_service/locking_events/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from django.utils import timezone

from eth_account import Account
from factory import LazyFunction, Sequence, SubFactory, fuzzy
from factory.django import DjangoModelFactory
from web3 import Web3

from safe_locking_service.locking_events.models import (
EthereumTx,
LockEvent,
UnlockEvent,
WithdrawnEvent,
)


class EthereumTxFactory(DjangoModelFactory):
class Meta:
model = EthereumTx

tx_hash = Sequence(lambda n: Web3.keccak(text=f"tx_hash-{n}").hex())
block_hash = Sequence(lambda n: Web3.keccak(text=f"tx_hash-{n}").hex())
block_number = Sequence(lambda n: n + 1)
block_timestamp = LazyFunction(timezone.now)


class LockEventFactory(DjangoModelFactory):
class Meta:
model = LockEvent

timestamp = LazyFunction(timezone.now)
ethereum_tx = SubFactory(EthereumTxFactory)
log_index = Sequence(lambda n: n)
amount = fuzzy.FuzzyInteger(0, 1000)
holder = LazyFunction(lambda: Account.create().address)


class UnlockEventFactory(DjangoModelFactory):
class Meta:
model = UnlockEvent

timestamp = LazyFunction(timezone.now)
ethereum_tx = SubFactory(EthereumTxFactory)
log_index = Sequence(lambda n: n)
holder = LazyFunction(lambda: Account.create().address)
amount = fuzzy.FuzzyInteger(0, 1000)
unlock_index = Sequence(lambda n: n + 1)


class WithdrawnEventFactory(DjangoModelFactory):
class Meta:
model = WithdrawnEvent

timestamp = LazyFunction(timezone.now)
ethereum_tx = SubFactory(EthereumTxFactory)
log_index = Sequence(lambda n: n)
holder = LazyFunction(lambda: Account.create().address)
amount = fuzzy.FuzzyInteger(0, 1000)
unlock_index = SubFactory(UnlockEventFactory)
61 changes: 61 additions & 0 deletions safe_locking_service/locking_events/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from django.db import IntegrityError
from django.test import TestCase

from eth_account import Account

from safe_locking_service.locking_events.models import (
EthereumTx,
LockEvent,
UnlockEvent,
WithdrawnEvent,
)
from safe_locking_service.locking_events.tests.factories import (
LockEventFactory,
UnlockEventFactory,
WithdrawnEventFactory,
)


class TestLockingModel(TestCase):
def test_create_lock_event(self):
safe_address = Account.create().address
ethereum_tx = LockEventFactory(holder=safe_address, amount=1000).ethereum_tx
self.assertEqual(EthereumTx.objects.count(), 1)
lock_event = LockEvent.objects.filter(
holder=safe_address, ethereum_tx=ethereum_tx
)[0]
self.assertEqual(lock_event.holder, safe_address)
self.assertEqual(lock_event.amount, 1000)

def test_create_unlock_event(self):
safe_address = Account.create().address
ethereum_tx = UnlockEventFactory(holder=safe_address, amount=1000).ethereum_tx
self.assertEqual(EthereumTx.objects.count(), 1)
unlock_event = UnlockEvent.objects.filter(
holder=safe_address, ethereum_tx=ethereum_tx
)[0]
self.assertEqual(unlock_event.holder, safe_address)
self.assertEqual(unlock_event.amount, 1000)
with self.assertRaisesMessage(IntegrityError, "violates unique constraint"):
UnlockEventFactory(
holder=safe_address, amount=1000, unlock_index=unlock_event.unlock_index
)

def test_create_withdrawn_event(self):
safe_address = Account.create().address
ethereum_tx = WithdrawnEventFactory(
holder=safe_address, amount=1000
).ethereum_tx
# Expected at least two transactions, one for unlock and other for withdrawn
self.assertEqual(EthereumTx.objects.count(), 2)
withdrawn_event = WithdrawnEvent.objects.filter(
holder=safe_address, ethereum_tx=ethereum_tx
)[0]
self.assertEqual(withdrawn_event.holder, safe_address)
self.assertEqual(withdrawn_event.amount, 1000)
with self.assertRaisesMessage(IntegrityError, "violates unique constraint"):
WithdrawnEventFactory(
holder=safe_address,
amount=1000,
unlock_index=withdrawn_event.unlock_index,
)
Loading