Skip to content

Commit

Permalink
Add model for locking events (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
moisses89 authored Feb 28, 2024
1 parent 48cc639 commit e527c19
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 4 deletions.
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,
)

0 comments on commit e527c19

Please sign in to comment.