Repository: safe-global/safe-config-service

Issue:
Add short name to chain info
Reference: https://github.com/gnosis/safe/issues/487

To support [EIP-3770](https://github.com/ethereum/EIPs/pull/3770) the client require the short name for each network. The short name is unique for each chain id and can be retrieved from [ethereum-lists/chains](https://github.com/ethereum-lists/chains) or https://chainid.network/shortNameMapping.json


Discussion:

Resolving patch:
diff --git a/src/chains/migrations/0029_chain_short_name.py b/src/chains/migrations/0029_chain_short_name.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/0029_chain_short_name.py
@@ -0,0 +1,269 @@
+# Generated by Django 3.2.7 on 2021-09-30 15:16
+
+from django.db import migrations, models
+
+
+def set_chain_short_name(apps, schema_editor):
+    Chain = apps.get_model("chains", "Chain")
+    for row in Chain.objects.all():
+        row.short_name = SHORT_NAME_MAPPING.get(row.id, "")
+        row.save(update_fields=["short_name"])
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("chains", "0028_chain_vpc_transaction_service_uri"),
+    ]
+
+    operations = [
+        # Add the field short_name with null=True in order to create a valid column
+        migrations.AddField(
+            model_name="chain",
+            name="short_name",
+            field=models.CharField(
+                unique=True,
+                null=True,
+                max_length=255,
+                verbose_name="EIP-3770 short name",
+            ),
+            preserve_default=False,
+        ),
+        # Fill the column with the values from SHORT_NAME_MAPPING (empty string if value is not found)
+        migrations.RunPython(set_chain_short_name, lambda apps, schema_editor: None),
+        # set the short_name column as non-nullable
+        migrations.AlterField(
+            model_name="chain",
+            name="short_name",
+            field=models.CharField(
+                unique=True,
+                null=False,
+                max_length=255,
+                verbose_name="EIP-3770 short name",
+            ),
+            preserve_default=False,
+        ),
+    ]
+
+
+# List retrieved from https://chainid.network/shortNameMapping.json on September 30, 2021
+SHORT_NAME_MAPPING = {
+    1: "eth",
+    2: "exp",
+    3: "rop",
+    4: "rin",
+    5: "gor",
+    6: "kot",
+    7: "tch",
+    8: "ubq",
+    9: "tubq",
+    10: "oeth",
+    11: "meta",
+    12: "kal",
+    13: "dstg",
+    14: "flr",
+    15: "diode",
+    16: "cflr",
+    17: "tfi",
+    18: "TST",
+    19: "sgb",
+    20: "elaeth",
+    21: "elaetht",
+    22: "eladid",
+    23: "eladidt",
+    30: "rsk",
+    31: "trsk",
+    32: "GooDT",
+    33: "GooD",
+    35: "tbwg",
+    38: "val",
+    40: "Telos EVM",
+    41: "Telos EVM Testnet",
+    42: "kov",
+    43: "darwinia",
+    44: "crab",
+    50: "xdc",
+    51: "TXDC",
+    52: "cet",
+    53: "tcet",
+    56: "bnb",
+    57: "sys",
+    58: "Ontology Mainnet",
+    59: "EOS Mainnet",
+    60: "go",
+    61: "etc",
+    62: "tetc",
+    63: "metc",
+    64: "ella",
+    65: "tokt",
+    66: "okt",
+    67: "dbm",
+    68: "SO1",
+    69: "okov",
+    76: "mix",
+    77: "poa",
+    78: "primuschain",
+    80: "GeneChain",
+    82: "Meter",
+    85: "gttest",
+    86: "gt",
+    88: "tomo",
+    95: "Kylin Testnet",
+    97: "bnbt",
+    99: "skl",
+    100: "xdai",
+    101: "eti",
+    102: "w3g",
+    108: "TT",
+    110: "xpr",
+    111: "ETL",
+    122: "fuse",
+    124: "dwu",
+    127: "feth",
+    128: "heco",
+    137: "matic",
+    142: "dax",
+    162: "tpht",
+    163: "pht",
+    170: "hoosmartchain",
+    172: "resil",
+    200: "aox",
+    211: "EDI",
+    246: "ewt",
+    250: "ftm",
+    256: "hecot",
+    269: "hpb",
+    321: "kcs",
+    322: "kcst",
+    361: "theta-mainnet",
+    363: "theta-sapphire",
+    364: "theta-amber",
+    365: "theta-testnet",
+    369: "pls",
+    385: "lisinski",
+    420: "ogor",
+    499: "rupx",
+    558: "tao",
+    595: "maca",
+    686: "kar",
+    721: "tfeth",
+    777: "cth",
+    787: "aca",
+    803: "haic",
+    820: "clo",
+    821: "tclo",
+    888: "wan",
+    940: "tpls",
+    977: "yeti",
+    999: "twan",
+    1001: "Baobab",
+    1007: "tnew",
+    1010: "EVC",
+    1012: "new",
+    1022: "sku",
+    1023: "tclv",
+    1024: "clv",
+    1139: "MATH",
+    1140: "tMATH",
+    1284: "mbeam",
+    1285: "mriver",
+    1286: "mrock",
+    1287: "mbase",
+    1288: "mshadow",
+    1618: "cate",
+    1620: "ath",
+    1856: "tsf",
+    1987: "egem",
+    2020: "420",
+    2021: "edg",
+    2022: "edgt",
+    2559: "ktoc",
+    4002: "tftm",
+    4689: "iotex-mainnet",
+    4690: "iotex-testnet",
+    5197: "es",
+    5700: "tsys",
+    5851: "Ontology Testnet",
+    5869: "rbd",
+    8029: "mdgl",
+    8080: "GeneChainAdn",
+    8217: "Cypress",
+    8285: "Kortho",
+    8723: "olo",
+    8724: "tolo",
+    8995: "berg",
+    10000: "smartbch",
+    10001: "smartbchtest",
+    10101: "GEN",
+    16000: "mtt",
+    16001: "mtttest",
+    24484: "web",
+    24734: "mintme",
+    31102: "esn",
+    31337: "got",
+    32659: "fsn",
+    39797: "nrg",
+    42069: "PC",
+    42161: "arb1",
+    42220: "CELO",
+    43110: "avaeth",
+    43113: "Fuji",
+    43114: "Avalanche",
+    44787: "ALFA",
+    49797: "tnrg",
+    62320: "BKLV",
+    71393: "ckb",
+    73799: "vt",
+    78110: "firenze",
+    80001: "maticmum",
+    100000: "qkc-r",
+    100001: "qkc-s0",
+    100002: "qkc-s1",
+    100003: "qkc-s2",
+    100004: "qkc-s3",
+    100005: "qkc-s4",
+    100006: "qkc-s5",
+    100007: "qkc-s6",
+    100008: "qkc-s7",
+    110000: "qkc-d-r",
+    110001: "qkc-d-s0",
+    110002: "qkc-d-s1",
+    110003: "qkc-d-s2",
+    110004: "qkc-d-s3",
+    110005: "qkc-d-s4",
+    110006: "qkc-d-s5",
+    110007: "qkc-d-s6",
+    110008: "qkc-d-s7",
+    200625: "aka",
+    246529: "ats",
+    246785: "atstau",
+    333888: "sparta",
+    333999: "olympus",
+    421611: "arb-rinkeby",
+    1313114: "etho",
+    1313500: "xero",
+    7762959: "music",
+    13371337: "tpep",
+    18289463: "ilt",
+    20181205: "qki",
+    28945486: "auxi",
+    35855456: "JOYS",
+    61717561: "aqua",
+    99415706: "TOYS",
+    311752642: "oneledger",
+    1122334455: "ipos",
+    1313161554: "aurora",
+    1313161555: "aurora-testnet",
+    1313161556: "aurora-betanet",
+    1666600000: "hmy-s0",
+    1666600001: "hmy-s1",
+    1666600002: "hmy-s2",
+    1666600003: "hmy-s3",
+    1666700000: "hmy-b-s0",
+    1666700001: "hmy-b-s1",
+    1666700002: "hmy-b-s2",
+    1666700003: "hmy-b-s3",
+    3125659152: "pirl",
+    4216137055: "frankenstein",
+    11297108099: "tpalm",
+    11297108109: "palm",
+}
diff --git a/src/chains/models.py b/src/chains/models.py
--- a/src/chains/models.py
+++ b/src/chains/models.py
@@ -33,6 +33,9 @@ class RpcAuthentication(models.TextChoices):
         default=100
     )  # A lower number will indicate more relevance
     name = models.CharField(verbose_name="Chain name", max_length=255)
+    short_name = models.CharField(
+        verbose_name="EIP-3770 short name", max_length=255, unique=True
+    )
     description = models.CharField(max_length=255, blank=True)
     l2 = models.BooleanField()
     rpc_authentication = models.CharField(
diff --git a/src/chains/serializers.py b/src/chains/serializers.py
--- a/src/chains/serializers.py
+++ b/src/chains/serializers.py
@@ -81,6 +81,7 @@ class BlockExplorerUriTemplateSerializer(serializers.Serializer):
 class ChainSerializer(serializers.ModelSerializer):
     chain_id = serializers.CharField(source="id")
     chain_name = serializers.CharField(source="name")
+    short_name = serializers.CharField()
     rpc_uri = serializers.SerializerMethodField()
     safe_apps_rpc_uri = serializers.SerializerMethodField()
     block_explorer_uri_template = serializers.SerializerMethodField()
@@ -98,6 +99,7 @@ class Meta:
         fields = [
             "chain_id",
             "chain_name",
+            "short_name",
             "description",
             "l2",
             "rpc_uri",


Test patch:
diff --git a/src/chains/migrations/tests/__init__.py b/src/chains/migrations/tests/__init__.py
new file mode 100644
diff --git a/src/chains/migrations/tests/test_migration_0029.py b/src/chains/migrations/tests/test_migration_0029.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/tests/test_migration_0029.py
@@ -0,0 +1,142 @@
+from .utils import TestMigrations
+
+
+class Migration0029TestCase(TestMigrations):
+    migrate_from = "0028_chain_vpc_transaction_service_uri"
+    migrate_to = "0029_chain_short_name"
+
+    def setUpBeforeMigration(self, apps):
+        Chain = apps.get_model("chains", "Chain")
+        Chain.objects.create(
+            id=1,
+            name="Mainnet",
+            description="",
+            l2=False,
+            rpc_authentication="API_KEY_PATH",
+            rpc_uri="https://mainnet.infura.io/v3/",
+            safe_apps_rpc_authentication="API_KEY_PATH",
+            safe_apps_rpc_uri="https://mainnet.infura.io/v3/",
+            block_explorer_uri_address_template="https://etherscan.io/address/{{address}}",
+            block_explorer_uri_tx_hash_template="https://etherscan.io/tx/{{txHash}}",
+            currency_name="Ether",
+            currency_symbol="ETH",
+            currency_decimals=18,
+            currency_logo_uri="https://gnosis-safe-token-logos.s3.amazonaws.com/ethereum-eth-logo.png",
+            transaction_service_uri="http://mainnet-safe-transaction-web.safe.svc.cluster.local",
+            vpc_transaction_service_uri="",
+            theme_text_color="#001428",
+            theme_background_color="#E8E7E6",
+            ens_registry_address="0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
+            recommended_master_copy_version="1.3.0",
+        )
+
+        Chain.objects.create(
+            id=56,
+            relevance=15,
+            name="Binance Smart Chain",
+            description="",
+            l2=True,
+            rpc_authentication="NO_AUTHENTICATION",
+            rpc_uri="https://bsc-dataseed.binance.org/",
+            safe_apps_rpc_authentication="NO_AUTHENTICATION",
+            safe_apps_rpc_uri="https://bsc-dataseed.binance.org/",
+            block_explorer_uri_address_template="https://bscscan.com/address/{{address}}",
+            block_explorer_uri_tx_hash_template="https://bscscan.com/tx/{{txHash}}",
+            currency_name="BNB",
+            currency_symbol="BNB",
+            currency_decimals=18,
+            currency_logo_uri="https://upload.wikimedia.org/wikipedia/commons/5/57/Binance_Logo.png",
+            transaction_service_uri="http://bsc-safe-transaction-web.safe.svc.cluster.local",
+            vpc_transaction_service_uri="",
+            theme_text_color="#ffffff",
+            theme_background_color="#fcc323",
+            ens_registry_address=None,
+            recommended_master_copy_version="1.3.0",
+        )
+
+        Chain.objects.create(
+            id=100,
+            relevance=5,
+            name="xDai",
+            description="",
+            l2=True,
+            rpc_authentication="NO_AUTHENTICATION",
+            rpc_uri="https://rpc.xdaichain.com/",
+            safe_apps_rpc_authentication="NO_AUTHENTICATION",
+            safe_apps_rpc_uri="https://rpc.xdaichain.com/",
+            block_explorer_uri_address_template="https://blockscout.com/xdai/mainnet/address/{{address}}/transactions",
+            block_explorer_uri_tx_hash_template="https://blockscout.com/xdai/mainnet/tx/{{txHash}}/",
+            currency_name="xDai",
+            currency_symbol="XDAI",
+            currency_decimals=18,
+            currency_logo_uri="https://gblobscdn.gitbook.com/",
+            transaction_service_uri="http://xdai-safe-transaction-web.safe.svc.cluster.local",
+            vpc_transaction_service_uri="",
+            theme_text_color="#ffffff",
+            theme_background_color="#48A9A6",
+            ens_registry_address=None,
+            recommended_master_copy_version="1.3.0",
+        )
+
+        Chain.objects.create(
+            id=137,
+            relevance=10,
+            name="Polygon",
+            description="",
+            l2=True,
+            rpc_authentication="API_KEY_PATH",
+            rpc_uri="https://polygon-mainnet.infura.io/v3/",
+            safe_apps_rpc_authentication="API_KEY_PATH",
+            safe_apps_rpc_uri="https://polygon-mainnet.infura.io/v3/",
+            block_explorer_uri_address_template="https://polygonscan.com/address/{{address}}",
+            block_explorer_uri_tx_hash_template="https://polygonscan.com/tx/{{txHash}}",
+            currency_name="Matic",
+            currency_symbol="MATIC",
+            currency_decimals=18,
+            currency_logo_uri="https://cryptologos.cc/logos/polygon-matic-logo.png",
+            transaction_service_uri="http://polygon-safe-transaction-web.safe.svc.cluster.local",
+            vpc_transaction_service_uri="",
+            theme_text_color="#ffffff",
+            theme_background_color="#8B50ED",
+            ens_registry_address=None,
+            recommended_master_copy_version="1.3.0",
+        )
+
+        Chain.objects.create(
+            id=246,
+            relevance=20,
+            name="Energy Web Chain",
+            description="",
+            l2=True,
+            rpc_authentication="NO_AUTHENTICATION",
+            rpc_uri="https://rpc.energyweb.org",
+            safe_apps_rpc_authentication="NO_AUTHENTICATION",
+            safe_apps_rpc_uri="https://rpc.energyweb.org",
+            block_explorer_uri_address_template="https://explorer.energyweb.org/address/{{address}}/transactions",
+            block_explorer_uri_tx_hash_template="https://explorer.energyweb.org/tx/{{txHash}}/internal-transactions",
+            currency_name="Energy Web Token",
+            currency_symbol="EWT",
+            currency_decimals=18,
+            currency_logo_uri="https://cryptologos.cc/logos/energy-web-token-ewt-logo.png?v=013",
+            transaction_service_uri="http://ewc-safe-transaction-web.safe.svc.cluster.local",
+            vpc_transaction_service_uri="",
+            theme_text_color="#ffffff",
+            theme_background_color="#A566FF",
+            ens_registry_address=None,
+            recommended_master_copy_version="1.3.0",
+        )
+
+    def test_short_name_set(self):
+        Chain = self.apps.get_model("chains", "Chain")
+
+        mainnet = Chain.objects.get(id=1)
+        binance = Chain.objects.get(id=56)
+        x_dai = Chain.objects.get(id=100)
+        polygon = Chain.objects.get(id=137)
+        ewt = Chain.objects.get(id=246)
+
+        self.assertEqual(mainnet.short_name, "eth")
+        self.assertEqual(binance.short_name, "bnb")
+        self.assertEqual(x_dai.short_name, "xdai")
+        self.assertEqual(polygon.short_name, "matic")
+        self.assertEqual(ewt.short_name, "ewt")
diff --git a/src/chains/migrations/tests/utils.py b/src/chains/migrations/tests/utils.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/tests/utils.py
@@ -0,0 +1,47 @@
+from django.apps import apps
+from django.db import connection
+from django.db.migrations.executor import MigrationExecutor
+from django.test import TestCase
+
+
+# Use this TestCase class in order to test migrations
+# migrate_from – the name of the migration where the test starts
+# migrate_to – the target migration
+#
+# Example:
+# You can set models on a specific migration inside your implementation of setUpBeforeMigration()
+# The validation can then be performed on each test function of the Test class
+# This class is heavily inspired by https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/
+class TestMigrations(TestCase):
+    @property
+    def app(self):
+        return apps.get_containing_app_config(type(self).__module__).name
+
+    migrate_from = None
+    migrate_to = None
+
+    def setUp(self):
+        assert (
+            self.migrate_from and self.migrate_to
+        ), "TestCase '{}' must define migrate_from and migrate_to     properties".format(
+            type(self).__name__
+        )
+        self.migrate_from = [(self.app, self.migrate_from)]
+        self.migrate_to = [(self.app, self.migrate_to)]
+        executor = MigrationExecutor(connection)
+        old_apps = executor.loader.project_state(self.migrate_from).apps
+
+        # Reverse to the original migration
+        executor.migrate(self.migrate_from)
+
+        self.setUpBeforeMigration(old_apps)
+
+        # Run the migration to test
+        executor = MigrationExecutor(connection)
+        executor.loader.build_graph()  # reload.
+        executor.migrate(self.migrate_to)
+
+        self.apps = executor.loader.project_state(self.migrate_to).apps
+
+    def setUpBeforeMigration(self, apps):
+        pass  # pragma: no cover
diff --git a/src/chains/tests/factories.py b/src/chains/tests/factories.py
--- a/src/chains/tests/factories.py
+++ b/src/chains/tests/factories.py
@@ -14,6 +14,7 @@ class Meta:
     id = factory.Sequence(lambda id: id)
     relevance = factory.Faker("pyint")
     name = factory.Faker("company")
+    short_name = factory.Faker("pystr", max_chars=255)
     description = factory.Faker("pystr", max_chars=255)
     l2 = factory.Faker("pybool")
     rpc_authentication = factory.lazy_attribute(
diff --git a/src/chains/tests/test_views.py b/src/chains/tests/test_views.py
--- a/src/chains/tests/test_views.py
+++ b/src/chains/tests/test_views.py
@@ -28,6 +28,7 @@ def test_json_payload_format(self):
                 {
                     "chainId": str(gas_price.chain.id),
                     "chainName": chain.name,
+                    "shortName": chain.short_name,
                     "description": chain.description,
                     "l2": chain.l2,
                     "rpcUri": {
@@ -135,6 +136,7 @@ def test_json_payload_format(self):
         json_response = {
             "chainId": str(chain.id),
             "chainName": chain.name,
+            "shortName": chain.short_name,
             "description": chain.description,
             "l2": chain.l2,
             "rpcUri": {
@@ -191,6 +193,7 @@ def test_match(self):
         json_response = {
             "chain_id": str(chain.id),
             "chain_name": chain.name,
+            "short_name": chain.short_name,
             "description": chain.description,
             "l2": chain.l2,
             "rpc_uri": {