Skip to content

Commit

Permalink
Add support for collection of oracles to chains endpoint (#210)
Browse files Browse the repository at this point in the history
- Breaking Change /api/v1/chains: each chain is now allowed to have multiple gas price configurations (resulting in a collection instead of a single object of fixed and oracle gas prices).
- The new GasPrice for each chain is now ranked – a lower value means that it'd show up higher on the list (eg.: rank 1 > rank 100)
- The new GasPrice collection is allowed to be empty – this is not only to reduce complexity but also because the current relationship between GasPrice <> Chain means that a Chain needs to exist first before assigning a GasPrice to it.
  • Loading branch information
fmrsabino authored Sep 6, 2021
1 parent 8783b96 commit 8ea9933
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 115 deletions.
14 changes: 13 additions & 1 deletion src/chains/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin

from .models import Chain
from .models import Chain, GasPrice


@admin.register(Chain)
Expand All @@ -17,3 +17,15 @@ class ChainAdmin(admin.ModelAdmin):
"relevance",
"name",
)


@admin.register(GasPrice)
class GasPrice(admin.ModelAdmin):
list_display = (
"chain_id",
"oracle_uri",
"fixed_wei_value",
"rank",
)
search_fields = ("chain_id", "oracle_uri")
ordering = ("rank",)
74 changes: 74 additions & 0 deletions src/chains/migrations/0023_create_gas_price_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated by Django 3.2.6 on 2021-09-01 12:38

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


def copy_gas_prices(apps, schema_editor):
GasPrice = apps.get_model("chains", "GasPrice")
Chain = apps.get_model("chains", "Chain")

GasPrice.objects.bulk_create(
GasPrice(
chain=chain,
oracle_uri=chain.gas_price_oracle_uri,
oracle_parameter=chain.gas_price_oracle_parameter,
gwei_factor=chain.gas_price_oracle_gwei_factor,
fixed_wei_value=chain.gas_price_fixed_wei,
)
for chain in Chain.objects.all()
)


class Migration(migrations.Migration):
dependencies = [
("chains", "0022_remove_chain_block_explorer_uri"),
]

operations = [
migrations.CreateModel(
name="GasPrice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("oracle_uri", models.URLField(blank=True, null=True)),
(
"oracle_parameter",
models.CharField(blank=True, max_length=255, null=True),
),
(
"gwei_factor",
models.DecimalField(
decimal_places=9,
default=1,
help_text="Factor required to reach the Gwei unit",
max_digits=19,
verbose_name="Gwei multiplier factor",
),
),
(
"fixed_wei_value",
gnosis.eth.django.models.Uint256Field(
blank=True, null=True, verbose_name="Fixed gas price (wei)"
),
),
("rank", models.SmallIntegerField(default=100)),
(
"chain",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="chains.chain"
),
),
],
),
# noop for backwards because it will be handled by the backwards of CreateModel (ie.: destroying the model)
migrations.RunPython(copy_gas_prices, migrations.RunPython.noop),
]
28 changes: 28 additions & 0 deletions src/chains/migrations/0024_remove_gas_price_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.6 on 2021-09-01 12:38

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("chains", "0023_create_gas_price_model"),
]

operations = [
migrations.RemoveField(
model_name="chain",
name="gas_price_fixed_wei",
),
migrations.RemoveField(
model_name="chain",
name="gas_price_oracle_gwei_factor",
),
migrations.RemoveField(
model_name="chain",
name="gas_price_oracle_parameter",
),
migrations.RemoveField(
model_name="chain",
name="gas_price_oracle_uri",
),
]
46 changes: 26 additions & 20 deletions src/chains/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,39 +57,45 @@ class RpcAuthentication(models.TextChoices):
help_text="Please use the following format: <em>#RRGGBB</em>.",
)
ens_registry_address = EthereumAddressField(null=True, blank=True)
gas_price_oracle_uri = models.URLField(blank=True, null=True)
gas_price_oracle_parameter = models.CharField(blank=True, null=True, max_length=255)
gas_price_oracle_gwei_factor = models.DecimalField(

recommended_master_copy_version = models.CharField(
max_length=255, validators=[sem_ver_validator]
)

def __str__(self):
return f"{self.name} | chain_id={self.id}"


class GasPrice(models.Model):
chain = models.ForeignKey(Chain, on_delete=models.CASCADE)
oracle_uri = models.URLField(blank=True, null=True)
oracle_parameter = models.CharField(blank=True, null=True, max_length=255)
gwei_factor = models.DecimalField(
default=1,
max_digits=19,
decimal_places=9,
verbose_name="Gwei multiplier factor",
help_text="Factor required to reach the Gwei unit",
)
gas_price_fixed_wei = Uint256Field(
fixed_wei_value = Uint256Field(
verbose_name="Fixed gas price (wei)", blank=True, null=True
)
recommended_master_copy_version = models.CharField(
max_length=255, validators=[sem_ver_validator]
)
rank = models.SmallIntegerField(
default=100
) # A lower number will indicate higher ranking

def __str__(self):
return f"Chain = {self.chain.id} | uri={self.oracle_uri} | fixed_wei_value={self.fixed_wei_value}"

def clean(self):
if (self.gas_price_fixed_wei is not None) == (
self.gas_price_oracle_uri is not None
):
if (self.fixed_wei_value is not None) == (self.oracle_uri is not None):
raise ValidationError(
{
"gas_price_oracle_uri": "An oracle uri or fixed gas price should be provided (but not both)",
"gas_price_fixed_wei": "An oracle uri or fixed gas price should be provided (but not both)",
"oracle_uri": "An oracle uri or fixed gas price should be provided (but not both)",
"fixed_wei_value": "An oracle uri or fixed gas price should be provided (but not both)",
}
)
if (
self.gas_price_oracle_uri is not None
and self.gas_price_oracle_parameter is None
):
if self.oracle_uri is not None and self.oracle_parameter is None:
raise ValidationError(
{"gas_price_oracle_parameter": "The oracle parameter should be set"}
{"oracle_parameter": "The oracle parameter should be set"}
)

def __str__(self):
return f"{self.name} | chain_id={self.id}"
36 changes: 20 additions & 16 deletions src/chains/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,26 @@

class GasPriceOracleSerializer(serializers.Serializer):
type = serializers.ReadOnlyField(default="oracle")
uri = serializers.URLField(source="gas_price_oracle_uri")
gas_parameter = serializers.CharField(source="gas_price_oracle_parameter")
gwei_factor = serializers.DecimalField(
source="gas_price_oracle_gwei_factor", max_digits=19, decimal_places=9
)
uri = serializers.URLField(source="oracle_uri")
gas_parameter = serializers.CharField(source="oracle_parameter")
gwei_factor = serializers.DecimalField(max_digits=19, decimal_places=9)


class GasPriceFixedSerializer(serializers.Serializer):
type = serializers.ReadOnlyField(default="fixed")
wei_value = serializers.CharField(source="gas_price_fixed_wei")
wei_value = serializers.CharField(source="fixed_wei_value")


class GasPriceSerializer(serializers.Serializer):
def to_representation(self, instance):
if instance.oracle_uri and instance.fixed_wei_value is None:
return GasPriceOracleSerializer(instance).data
elif instance.fixed_wei_value and instance.oracle_uri is None:
return GasPriceFixedSerializer(instance).data
else:
raise APIException(
f"The gas price oracle or a fixed gas price was not provided for chain {instance.chain}"
)


class ThemeSerializer(serializers.Serializer):
Expand Down Expand Up @@ -123,13 +133,7 @@ def get_rpc_uri(obj):
def get_block_explorer_uri_template(obj):
return BlockExplorerUriTemplateSerializer(obj).data

@staticmethod
def get_gas_price(obj):
if obj.gas_price_oracle_uri and obj.gas_price_fixed_wei is None:
return GasPriceOracleSerializer(obj).data
elif obj.gas_price_fixed_wei and obj.gas_price_oracle_uri is None:
return GasPriceFixedSerializer(obj).data
else:
raise APIException(
f"The gas price oracle or a fixed gas price was not provided for chain {obj.id}"
)
@swagger_serializer_method(serializer_or_field=GasPriceSerializer)
def get_gas_price(self, instance):
ranked_gas_prices = instance.gasprice_set.all().order_by("rank")
return GasPriceSerializer(ranked_gas_prices, many=True).data
20 changes: 14 additions & 6 deletions src/chains/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import web3
from factory.django import DjangoModelFactory

from ..models import Chain
from ..models import Chain, GasPrice


class ChainFactory(DjangoModelFactory):
Expand All @@ -31,13 +31,21 @@ class Meta:
transaction_service_uri = factory.Faker("url")
theme_text_color = factory.Faker("hex_color")
theme_background_color = factory.Faker("hex_color")
gas_price_oracle_uri = None
gas_price_oracle_parameter = None
ens_registry_address = factory.LazyAttribute(
lambda o: web3.Account.create().address
)
gas_price_oracle_gwei_factor = factory.Faker(
recommended_master_copy_version = "1.3.0"


class GasPriceFactory(DjangoModelFactory):
class Meta:
model = GasPrice

chain = factory.SubFactory(ChainFactory)
oracle_uri = None
oracle_parameter = None
gwei_factor = factory.Faker(
"pydecimal", positive=True, min_value=1, max_value=1_000_000_000, right_digits=9
)
gas_price_fixed_wei = factory.Faker("pyint")
recommended_master_copy_version = "1.3.0"
fixed_wei_value = factory.Faker("pyint")
rank = factory.Faker("pyint")
Loading

0 comments on commit 8ea9933

Please sign in to comment.