Repository: safe-global/safe-config-service

Issue:
Add a property for Chain to hide
**Background**
- Adding a chain is a very tedious process
- We need to add chains temporary to do proper QA before releasing
- After adding a chain, removing and doing QA it could be possible that when we add it again we make a mistake and chain is not working as expected

**Describe the solution you'd like**
- A `hidden` field that will remove a `Chain` from every result



Discussion:

Resolving patch:
diff --git a/src/chains/migrations/0037_chain_hidden.py b/src/chains/migrations/0037_chain_hidden.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/0037_chain_hidden.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.0.5 on 2023-04-19 14:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("chains", "0036_alter_chain_transaction_service_uri_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="chain",
+            name="hidden",
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/src/chains/models.py b/src/chains/models.py
--- a/src/chains/models.py
+++ b/src/chains/models.py
@@ -115,10 +115,10 @@ class RpcAuthentication(models.TextChoices):
         help_text="Please use the following format: <em>#RRGGBB</em>.",
     )
     ens_registry_address = EthereumAddressField(null=True, blank=True)  # type: ignore[no-untyped-call]
-
     recommended_master_copy_version = models.CharField(
         max_length=255, validators=[sem_ver_validator]
     )
+    hidden = models.BooleanField(default=False)
 
     def get_disabled_wallets(self) -> QuerySet["Wallet"]:
         all_wallets = Wallet.objects.all()
diff --git a/src/chains/views.py b/src/chains/views.py
--- a/src/chains/views.py
+++ b/src/chains/views.py
@@ -19,7 +19,7 @@ class ChainsPagination(LimitOffsetPagination):
 class ChainsListView(ListAPIView):  # type: ignore[type-arg]
     serializer_class = ChainSerializer
     pagination_class = ChainsPagination
-    queryset = Chain.objects.all()
+    queryset = Chain.objects.filter(hidden=False)
     filter_backends = [filters.OrderingFilter]
     ordering_fields = ["relevance", "name"]
     ordering = [
@@ -30,7 +30,7 @@ class ChainsListView(ListAPIView):  # type: ignore[type-arg]
 
 class ChainsDetailView(RetrieveAPIView):  # type: ignore[type-arg]
     serializer_class = ChainSerializer
-    queryset = Chain.objects.all()
+    queryset = Chain.objects.filter(hidden=False)
 
     @swagger_auto_schema(
         operation_id="Get chain by id"
@@ -42,7 +42,7 @@ def get(self, request: Request, *args: Any, **kwargs: Any) -> Response:
 class ChainsDetailViewByShortName(RetrieveAPIView):  # type: ignore[type-arg]
     lookup_field = "short_name"
     serializer_class = ChainSerializer
-    queryset = Chain.objects.all()
+    queryset = Chain.objects.filter(hidden=False)
 
     @swagger_auto_schema(
         operation_id="Get chain by shortName",


Test patch:
diff --git a/src/chains/migrations/tests/test_migration_0037.py b/src/chains/migrations/tests/test_migration_0037.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/tests/test_migration_0037.py
@@ -0,0 +1,67 @@
+from django.db.migrations.state import StateApps
+
+from chains.migrations.tests.utils import TestMigrations
+
+
+class Migration0034TestCase(TestMigrations):
+    migrate_from = "0036_alter_chain_transaction_service_uri_and_more"
+    migrate_to = "0037_chain_hidden"
+
+    def setUpBeforeMigration(self, apps: StateApps) -> None:
+        Chain = apps.get_model("chains", "Chain")
+        Chain.objects.create(
+            id=1,
+            name="Mainnet",
+            short_name="eth",
+            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=100,
+            relevance=5,
+            name="xDai",
+            short_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",
+        )
+
+    def test_chain_accessible(self) -> None:
+        Chain = self.apps_registry.get_model("chains", "Chain")
+
+        mainnet = Chain.objects.get(id=1)
+        xDai = Chain.objects.get(id=100)
+
+        self.assertEqual(mainnet.hidden, False)
+        self.assertEqual(xDai.hidden, False)
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
@@ -44,6 +44,7 @@ class Meta:
         lambda o: web3.Account.create().address
     )
     recommended_master_copy_version = "1.3.0"
+    hidden = False
 
 
 class GasPriceFactory(DjangoModelFactory):  # type: ignore[misc]
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
@@ -210,6 +210,14 @@ def test_no_match(self) -> None:
 
         self.assertEqual(response.status_code, 404)
 
+    def test_hidden_chain_no_match(self) -> None:
+        ChainFactory.create(id=1, hidden=True)
+        url = reverse("v1:chains:detail", args=[1])
+
+        response = self.client.get(path=url, data=None, format="json")
+
+        self.assertEqual(response.status_code, 404)
+
     def test_by_short_name(self) -> None:
         ChainFactory.create(id=1, short_name="eth")
         url_by_id = reverse("v1:chains:detail", args=[1])
@@ -224,6 +232,16 @@ def test_by_short_name(self) -> None:
         self.assertEqual(response_by_short_name.status_code, 200)
         self.assertEqual(response_by_id.json(), response_by_short_name.json())
 
+    def test_hidden_chain_by_short_name_no_match(self) -> None:
+        ChainFactory.create(id=1, short_name="eth", hidden=True)
+        url_by_short_name = reverse("v1:chains:detail_by_short_name", args=["eth"])
+
+        response_by_short_name = self.client.get(
+            path=url_by_short_name, data=None, format="json"
+        )
+
+        self.assertEqual(response_by_short_name.status_code, 404)
+
 
 class ChainsListViewRelevanceTests(APITestCase):
     def test_relevance_sorting(self) -> None:
@@ -250,6 +268,20 @@ def test_same_relevance_sorting(self) -> None:
         self.assertEqual(chain_ids, [str(chain_3.id), str(chain_2.id), str(chain_1.id)])
 
 
+class ChainsListViewHiddenTests(APITestCase):
+    def test_hidden_chains_get_ignored(self) -> None:
+        ChainFactory.create_batch(5)
+        ChainFactory.create(hidden=True)
+        ChainFactory.create(hidden=True)
+        url = reverse("v1:chains:list")
+
+        response = self.client.get(path=url, data=None, format="json")
+
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json()["count"], 5)
+        self.assertEqual(len(response.json()["results"]), 5)
+
+
 class ChainsEnsRegistryTests(APITestCase):
     def test_null_ens_registry_address(self) -> None:
         ChainFactory.create(id=1, ens_registry_address=None)