From 8922d0536d05c34c1d0570ad3285cf047a13073e Mon Sep 17 00:00:00 2001 From: Frederico Sabino <3332770+fmrsabino@users.noreply.github.com> Date: Wed, 13 Apr 2022 14:27:27 +0200 Subject: [PATCH] Add tags to SafeApp (#851) - Enables serialisation and deserialisation of tags for a SafeApp - Serialisation is enabled IFF SAFE_APPS_TAGS_FEATURE_ENABLED is set to true --- .env.sample | 3 + src/common/converters/tests/safe_app.rs | 5 + src/common/models/backend/safe_apps.rs | 3 + src/config/mod.rs | 4 + src/routes/safe_apps/converters.rs | 1 + src/routes/safe_apps/models.rs | 10 ++ .../json/response_safe_apps_with_tags.json | 91 +++++++++++++++++++ src/routes/safe_apps/tests/mod.rs | 2 + src/routes/safe_apps/tests/routes.rs | 48 ++++++++++ src/tests/json/mod.rs | 2 + .../polygon_safe_apps_with_tags.json | 69 ++++++++++++++ 11 files changed, 238 insertions(+) create mode 100644 src/routes/safe_apps/tests/json/response_safe_apps_with_tags.json create mode 100644 src/tests/json/safe_apps/polygon_safe_apps_with_tags.json diff --git a/.env.sample b/.env.sample index 9c9e6c475..b99570b60 100644 --- a/.env.sample +++ b/.env.sample @@ -12,6 +12,9 @@ NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx/ FEATURE_FLAG_NESTED_DECODING=true FEATURE_FLAG_BALANCES_RATE_IMPLEMENTATION=false +# Enables the tags feature for Safe Apps +SAFE_APPS_TAGS_FEATURE_ENABLED=false + # Random string (generated with openssl rand -base64 32) # [string] a 256-bit base64 encoded string (44 characters) to use as the secret key ROCKET_SECRET_KEY=Qt6DPFUU8qO4BKTCQnKAgt9FBBJxIWAYUGyHuruVfpE= diff --git a/src/common/converters/tests/safe_app.rs b/src/common/converters/tests/safe_app.rs index 72951dd6b..c63e7890a 100644 --- a/src/common/converters/tests/safe_app.rs +++ b/src/common/converters/tests/safe_app.rs @@ -39,6 +39,7 @@ fn safe_apps_several_apps() { chain_ids: vec!["1".to_string(), "137".to_string()], provider: None, access_control: SafeAppAccessControlPolicies::NoRestrictions, + tags: vec![] }, SafeApp { id: 24, @@ -49,6 +50,7 @@ fn safe_apps_several_apps() { chain_ids: vec!["1".to_string(), "4".to_string(),"10".to_string(),"56".to_string(),"100".to_string(),"137".to_string(),"246".to_string(), "42161".to_string(), "43114".to_string(), "73799".to_string()], provider: None, access_control: SafeAppAccessControlPolicies::NoRestrictions, + tags: vec![] }, SafeApp { id: 11, @@ -64,6 +66,7 @@ fn safe_apps_several_apps() { access_control: SafeAppAccessControlPolicies::DomainAllowlist(SafeAppDomainAllowlistPolicy { value: vec!["https://gnosis-safe.io".to_string(), "https://dev.gnosis-safe.io".to_string()], }), + tags: vec![] }, SafeApp { id: 30, @@ -74,6 +77,7 @@ fn safe_apps_several_apps() { chain_ids: vec!["1".to_string(),"56".to_string(),"137".to_string()], provider: None, access_control: SafeAppAccessControlPolicies::NoRestrictions, + tags: vec![] }, SafeApp { id: 25, @@ -84,6 +88,7 @@ fn safe_apps_several_apps() { chain_ids: vec!["1".to_string(), "4".to_string(), "10".to_string(),"56".to_string(),"100".to_string(),"137".to_string(),"246".to_string(), "73799".to_string(), "42161".to_string(), "43114".to_string()], provider: None, access_control: SafeAppAccessControlPolicies::NoRestrictions, + tags: vec![] }, ]; diff --git a/src/common/models/backend/safe_apps.rs b/src/common/models/backend/safe_apps.rs index 1e47160c9..ae2304bf0 100644 --- a/src/common/models/backend/safe_apps.rs +++ b/src/common/models/backend/safe_apps.rs @@ -11,6 +11,9 @@ pub struct SafeApp { pub chain_ids: Vec, pub provider: Option, pub access_control: SafeAppAccessControlPolicies, + // We set this value as a default since this feature might not be enabled yet. See SAFE_APPS_TAGS_FEATURE_ENABLED + #[serde(default)] + pub tags: Vec, } #[derive(Deserialize, Debug, PartialEq, Clone)] diff --git a/src/config/mod.rs b/src/config/mod.rs index aa836bccb..881f4014e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -173,6 +173,10 @@ pub fn feature_flag_balances_rate_implementation() -> bool { env_with_default("FEATURE_FLAG_BALANCES_RATE_IMPLEMENTATION", false) } +pub fn is_safe_apps_tags_feature_enabled() -> bool { + env_with_default("SAFE_APPS_TAGS_FEATURE_ENABLED", false) +} + pub fn vpc_transaction_service_uri() -> bool { env_with_default("VPC_TRANSACTION_SERVICE_URI", true) } diff --git a/src/routes/safe_apps/converters.rs b/src/routes/safe_apps/converters.rs index f63cc573b..5fab68a02 100644 --- a/src/routes/safe_apps/converters.rs +++ b/src/routes/safe_apps/converters.rs @@ -33,6 +33,7 @@ impl From for SafeApp { } _ => SafeAppAccessControlPolicies::Unknown, }, + tags: safe_app.tags, } } } diff --git a/src/routes/safe_apps/models.rs b/src/routes/safe_apps/models.rs index ce24437f0..7aaa6b30a 100644 --- a/src/routes/safe_apps/models.rs +++ b/src/routes/safe_apps/models.rs @@ -1,5 +1,7 @@ use serde::Serialize; +use crate::config::is_safe_apps_tags_feature_enabled; + #[derive(Serialize, Debug, PartialEq, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(test, derive(serde::Deserialize))] @@ -12,6 +14,14 @@ pub struct SafeApp { pub chain_ids: Vec, pub provider: Option, pub access_control: SafeAppAccessControlPolicies, + #[serde(skip_serializing_if = "should_skip_serializing_tags")] + // We deserialize this for testing so it would break since the value wouldn't be present + #[serde(default)] + pub tags: Vec, +} + +pub fn should_skip_serializing_tags(_tags: &Vec) -> bool { + return !is_safe_apps_tags_feature_enabled(); } #[derive(Serialize, Debug, PartialEq, Clone)] diff --git a/src/routes/safe_apps/tests/json/response_safe_apps_with_tags.json b/src/routes/safe_apps/tests/json/response_safe_apps_with_tags.json new file mode 100644 index 000000000..80e5fde1b --- /dev/null +++ b/src/routes/safe_apps/tests/json/response_safe_apps_with_tags.json @@ -0,0 +1,91 @@ +[ + { + "id": 26, + "url": "https://curve.fi", + "name": "Curve", + "iconUrl": "https://curve.fi/logo-square.svg", + "description": "Decentralized exchange liquidity pool designed for extremely efficient stablecoin trading and low-risk income for liquidity providers", + "chainIds": ["1", "137"], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + }, + "tags": ["tag1"] + }, + { + "id": 24, + "url": "https://safe-apps.dev.gnosisdev.com/tx-builder", + "name": "Transaction Builder", + "iconUrl": "https://safe-apps.dev.gnosisdev.com/tx-builder/tx-builder.png", + "description": "A Safe app to compose custom transactions", + "chainIds": [ + "1", + "4", + "10", + "56", + "100", + "137", + "246", + "42161", + "43114", + "73799" + ], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + }, + "tags": ["tag2"] + }, + { + "id": 11, + "url": "https://app.1inch.io", + "name": "1inch.exchange", + "iconUrl": "https://app.1inch.io/assets/images/1inch.svg", + "description": "The most efficient defi aggregator", + "chainIds": ["1", "56", "137"], + "provider": { + "url": "https://1inch.exchange", + "name": "1inch corporation" + }, + "accessControl": { + "type": "DOMAIN_ALLOWLIST", + "value": ["https://gnosis-safe.io", "https://dev.gnosis-safe.io"] + } + }, + { + "id": 30, + "url": "https://paraswap.io", + "name": "ParaSwap", + "iconUrl": "https://paraswap.io/paraswap.svg", + "description": "ParaSwap allows dApps and traders to get the best DEX liquidity by aggregating multiple markets and offering the best rates", + "chainIds": ["1", "56", "137"], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + } + }, + { + "id": 25, + "url": "https://safe-apps.dev.gnosisdev.com/wallet-connect", + "name": "WalletConnect", + "iconUrl": "https://safe-apps.dev.gnosisdev.com/wallet-connect/wallet-connect.svg", + "description": "Connect your Safe to any dApp that supports WalletConnect", + "chainIds": [ + "1", + "4", + "10", + "56", + "100", + "137", + "246", + "73799", + "42161", + "43114" + ], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + }, + "tags": ["tag1", "tag2"] + } +] diff --git a/src/routes/safe_apps/tests/mod.rs b/src/routes/safe_apps/tests/mod.rs index 5f9cc2d93..4d3cc139d 100644 --- a/src/routes/safe_apps/tests/mod.rs +++ b/src/routes/safe_apps/tests/mod.rs @@ -1,3 +1,5 @@ mod routes; pub(crate) const RESPONSE_SAFE_APPS: &str = include_str!("json/response_safe_apps.json"); +pub(crate) const RESPONSE_SAFE_APPS_WITH_TAGS: &str = + include_str!("json/response_safe_apps_with_tags.json"); diff --git a/src/routes/safe_apps/tests/routes.rs b/src/routes/safe_apps/tests/routes.rs index d38e8aba6..03fa8c074 100644 --- a/src/routes/safe_apps/tests/routes.rs +++ b/src/routes/safe_apps/tests/routes.rs @@ -1,4 +1,5 @@ use crate::routes::safe_apps::models::SafeApp; +use crate::routes::safe_apps::tests::RESPONSE_SAFE_APPS_WITH_TAGS; use crate::tests::main::setup_rocket; use crate::utils::errors::{ApiError, ErrorDetails}; use crate::utils::http_client::{MockHttpClient, Request, Response}; @@ -6,11 +7,13 @@ use mockall::predicate::eq; use rocket::http::{Header, Status}; use rocket::local::asynchronous::Client; use serde_json::json; +use std::env; use super::RESPONSE_SAFE_APPS; #[rocket::async_test] async fn safe_apps() { + env::set_var("SAFE_APPS_TAGS_FEATURE_ENABLED", "false"); let chain_id = "137"; let client_url = "https://gnosis-safe.io"; @@ -58,6 +61,7 @@ async fn safe_apps() { #[rocket::async_test] async fn safe_apps_not_found() { + env::set_var("SAFE_APPS_TAGS_FEATURE_ENABLED", "false"); let chain_id = "4"; let backend_error_json = json!({"details": "Not found"}).to_string(); let error = ErrorDetails { @@ -105,3 +109,47 @@ async fn safe_apps_not_found() { serde_json::to_string(&error).unwrap() ); } + +#[rocket::async_test] +async fn safe_apps_tags() { + env::set_var("SAFE_APPS_TAGS_FEATURE_ENABLED", "true"); + let chain_id = "137"; + let client_url = "https://gnosis-safe.io"; + let mut mock_http_client = MockHttpClient::new(); + let safe_apps_request = Request::new(config_uri!( + "/v1/safe-apps/?chainId={}&clientUrl={}", + chain_id, + client_url + )); + mock_http_client + .expect_get() + .times(1) + .with(eq(safe_apps_request)) + .return_once(move |_| { + Ok(Response { + status_code: 200, + body: String::from(crate::tests::json::POLYGON_SAFE_APPS_WITH_TAGS), + }) + }); + let client = Client::tracked( + setup_rocket( + mock_http_client, + routes![super::super::routes::get_safe_apps], + ) + .await, + ) + .await + .expect("valid rocket instance"); + let response = { + let mut response = client.get("/v1/chains/137/safe-apps?client_url=https://gnosis-safe.io"); + response.add_header(Header::new("Host", "test.gnosis.io")); + response.dispatch().await + }; + let actual_status = response.status(); + let actual_body = response.into_string().await.unwrap(); + let actual: Vec = serde_json::from_str(&actual_body).unwrap(); + let expected: Vec = serde_json::from_str(RESPONSE_SAFE_APPS_WITH_TAGS).unwrap(); + + assert_eq!(actual_status, Status::Ok); + assert_eq!(actual, expected); +} diff --git a/src/tests/json/mod.rs b/src/tests/json/mod.rs index e6d5f9b8f..6940b0d50 100644 --- a/src/tests/json/mod.rs +++ b/src/tests/json/mod.rs @@ -150,6 +150,8 @@ pub const CHAIN_INFO_RINKEBY_ENABLED_FEATURES: &str = include_str!("chains/rinkeby_enabled_features.json"); pub const POLYGON_SAFE_APPS: &str = include_str!("safe_apps/polygon_safe_apps.json"); +pub const POLYGON_SAFE_APPS_WITH_TAGS: &str = + include_str!("safe_apps/polygon_safe_apps_with_tags.json"); pub const UNISWAP_SAFE_APP_MANIFEST: &str = include_str!("safe_apps/uniswap_manifest.json"); pub const POLYGON_MASTER_COPIES: &str = include_str!("master_copies/polygon_master_copies.json"); diff --git a/src/tests/json/safe_apps/polygon_safe_apps_with_tags.json b/src/tests/json/safe_apps/polygon_safe_apps_with_tags.json new file mode 100644 index 000000000..ffbd4fdfd --- /dev/null +++ b/src/tests/json/safe_apps/polygon_safe_apps_with_tags.json @@ -0,0 +1,69 @@ +[ + { + "id": 26, + "url": "https://curve.fi", + "name": "Curve", + "iconUrl": "https://curve.fi/logo-square.svg", + "description": "Decentralized exchange liquidity pool designed for extremely efficient stablecoin trading and low-risk income for liquidity providers", + "chainIds": [1, 137], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + }, + "tags": ["tag1"] + }, + { + "id": 24, + "url": "https://safe-apps.dev.gnosisdev.com/tx-builder", + "name": "Transaction Builder", + "iconUrl": "https://safe-apps.dev.gnosisdev.com/tx-builder/tx-builder.png", + "description": "A Safe app to compose custom transactions", + "chainIds": [1, 4, 10, 56, 100, 137, 246, 42161, 43114, 73799], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + }, + "tags": ["tag2"] + }, + { + "id": 11, + "url": "https://app.1inch.io", + "name": "1inch.exchange", + "iconUrl": "https://app.1inch.io/assets/images/1inch.svg", + "description": "The most efficient defi aggregator", + "chainIds": [1, 56, 137], + "provider": { + "url": "https://1inch.exchange", + "name": "1inch corporation" + }, + "accessControl": { + "type": "DOMAIN_ALLOWLIST", + "value": ["https://gnosis-safe.io", "https://dev.gnosis-safe.io"] + } + }, + { + "id": 30, + "url": "https://paraswap.io", + "name": "ParaSwap", + "iconUrl": "https://paraswap.io/paraswap.svg", + "description": "ParaSwap allows dApps and traders to get the best DEX liquidity by aggregating multiple markets and offering the best rates", + "chainIds": [1, 56, 137], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + } + }, + { + "id": 25, + "url": "https://safe-apps.dev.gnosisdev.com/wallet-connect", + "name": "WalletConnect", + "iconUrl": "https://safe-apps.dev.gnosisdev.com/wallet-connect/wallet-connect.svg", + "description": "Connect your Safe to any dApp that supports WalletConnect", + "chainIds": [1, 4, 10, 56, 100, 137, 246, 73799, 42161, 43114], + "provider": null, + "accessControl": { + "type": "NO_RESTRICTIONS" + }, + "tags": ["tag1", "tag2"] + } +]