|
1 | | -use serde::{Deserialize, Serialize}; |
2 | | -use strum::{AsRefStr, EnumString}; |
| 1 | +use url::Url; |
3 | 2 |
|
4 | | -pub const DEEP_LINK_SCHEME: &str = "gem://"; |
| 3 | +use crate::AssetId; |
5 | 4 |
|
6 | | -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, AsRefStr, EnumString)] |
7 | | -#[strum(serialize_all = "camelCase")] |
8 | | -#[serde(rename_all = "camelCase")] |
| 5 | +const DEEPLINK_HOST: &str = "gemwallet.com"; |
| 6 | +const DEEPLINK_WEB_SCHEME: &str = "https"; |
| 7 | +const DEEPLINK_GEM_SCHEME: &str = "gem"; |
| 8 | + |
| 9 | +const PATH_TOKENS: &str = "tokens"; |
| 10 | +const PATH_PERPETUALS: &str = "perpetuals"; |
| 11 | +const PATH_REWARDS: &str = "rewards"; |
| 12 | +const PATH_JOIN: &str = "join"; |
| 13 | + |
| 14 | +const QUERY_CODE: &str = "code"; |
| 15 | + |
| 16 | +#[derive(Debug, Clone, PartialEq)] |
9 | 17 | pub enum Deeplink { |
10 | | - Rewards, |
| 18 | + Asset { asset_id: AssetId }, |
| 19 | + Perpetuals, |
| 20 | + Rewards { code: Option<String> }, |
11 | 21 | } |
12 | 22 |
|
13 | 23 | impl Deeplink { |
14 | 24 | pub fn to_url(&self) -> String { |
15 | | - format!("{}{}", DEEP_LINK_SCHEME, self.as_ref()) |
| 25 | + format!("{DEEPLINK_WEB_SCHEME}://{DEEPLINK_HOST}{}", self.path()) |
| 26 | + } |
| 27 | + |
| 28 | + pub fn to_gem_url(&self) -> String { |
| 29 | + format!("{DEEPLINK_GEM_SCHEME}://{}", self.path().trim_start_matches('/')) |
| 30 | + } |
| 31 | + |
| 32 | + pub fn from_url(url: &str) -> Option<Self> { |
| 33 | + let url = Url::parse(url).ok()?; |
| 34 | + let segments = url_segments(&url)?; |
| 35 | + let (component, params) = segments.split_first()?; |
| 36 | + |
| 37 | + let deeplink = match component.as_str() { |
| 38 | + PATH_TOKENS => Deeplink::Asset { |
| 39 | + asset_id: AssetId::from(params.first()?.parse().ok()?, params.get(1).cloned()), |
| 40 | + }, |
| 41 | + PATH_PERPETUALS => Deeplink::Perpetuals, |
| 42 | + PATH_REWARDS | PATH_JOIN => Deeplink::Rewards { |
| 43 | + code: params.first().cloned().or_else(|| query_value(&url, QUERY_CODE)), |
| 44 | + }, |
| 45 | + _ => return None, |
| 46 | + }; |
| 47 | + Some(deeplink) |
| 48 | + } |
| 49 | + |
| 50 | + fn path(&self) -> String { |
| 51 | + match self { |
| 52 | + Deeplink::Asset { asset_id } => match &asset_id.token_id { |
| 53 | + Some(token_id) => format!("/{PATH_TOKENS}/{}/{token_id}", asset_id.chain.as_ref()), |
| 54 | + None => format!("/{PATH_TOKENS}/{}", asset_id.chain.as_ref()), |
| 55 | + }, |
| 56 | + Deeplink::Perpetuals => format!("/{PATH_PERPETUALS}"), |
| 57 | + Deeplink::Rewards { code } => path_with_query(PATH_REWARDS, QUERY_CODE, code.clone()), |
| 58 | + } |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +fn path_with_query(component: &str, query_key: &str, query_value: Option<String>) -> String { |
| 63 | + match query_value { |
| 64 | + Some(value) => format!("/{component}?{query_key}={value}"), |
| 65 | + None => format!("/{component}"), |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +fn url_segments(url: &Url) -> Option<Vec<String>> { |
| 70 | + let mut segments: Vec<String> = url |
| 71 | + .path_segments() |
| 72 | + .map(|parts| parts.filter(|part| !part.is_empty()).map(String::from).collect()) |
| 73 | + .unwrap_or_default(); |
| 74 | + |
| 75 | + match url.scheme() { |
| 76 | + DEEPLINK_WEB_SCHEME => { |
| 77 | + if url.host_str()? != DEEPLINK_HOST { |
| 78 | + return None; |
| 79 | + } |
| 80 | + } |
| 81 | + DEEPLINK_GEM_SCHEME => { |
| 82 | + if let Some(host) = url.host_str().filter(|host| !host.is_empty()) { |
| 83 | + segments.insert(0, host.to_string()); |
| 84 | + } |
| 85 | + } |
| 86 | + _ => return None, |
| 87 | + } |
| 88 | + Some(segments) |
| 89 | +} |
| 90 | + |
| 91 | +fn query_value(url: &Url, key: &str) -> Option<String> { |
| 92 | + url.query_pairs().find(|(query_key, _)| query_key.as_ref() == key).map(|(_, value)| value.into_owned()) |
| 93 | +} |
| 94 | + |
| 95 | +#[cfg(test)] |
| 96 | +mod tests { |
| 97 | + use super::*; |
| 98 | + use crate::Chain; |
| 99 | + |
| 100 | + #[test] |
| 101 | + fn test_to_url() { |
| 102 | + assert_eq!( |
| 103 | + Deeplink::Asset { |
| 104 | + asset_id: AssetId::from_chain(Chain::Bitcoin) |
| 105 | + } |
| 106 | + .to_url(), |
| 107 | + "https://gemwallet.com/tokens/bitcoin" |
| 108 | + ); |
| 109 | + assert_eq!( |
| 110 | + Deeplink::Asset { |
| 111 | + asset_id: AssetId::token(Chain::Ethereum, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), |
| 112 | + } |
| 113 | + .to_url(), |
| 114 | + "https://gemwallet.com/tokens/ethereum/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" |
| 115 | + ); |
| 116 | + assert_eq!(Deeplink::Perpetuals.to_url(), "https://gemwallet.com/perpetuals"); |
| 117 | + assert_eq!(Deeplink::Rewards { code: None }.to_url(), "https://gemwallet.com/rewards"); |
| 118 | + assert_eq!( |
| 119 | + Deeplink::Rewards { |
| 120 | + code: Some("gemcoder".to_string()), |
| 121 | + } |
| 122 | + .to_url(), |
| 123 | + "https://gemwallet.com/rewards?code=gemcoder" |
| 124 | + ); |
| 125 | + } |
| 126 | + |
| 127 | + #[test] |
| 128 | + fn test_to_gem_url() { |
| 129 | + assert_eq!(Deeplink::Rewards { code: None }.to_gem_url(), "gem://rewards"); |
| 130 | + assert_eq!(Deeplink::Perpetuals.to_gem_url(), "gem://perpetuals"); |
| 131 | + assert_eq!( |
| 132 | + Deeplink::Asset { |
| 133 | + asset_id: AssetId::from_chain(Chain::Bitcoin) |
| 134 | + } |
| 135 | + .to_gem_url(), |
| 136 | + "gem://tokens/bitcoin" |
| 137 | + ); |
| 138 | + } |
| 139 | + |
| 140 | + #[test] |
| 141 | + fn test_from_url() { |
| 142 | + assert_eq!( |
| 143 | + Deeplink::from_url("https://gemwallet.com/tokens/bitcoin"), |
| 144 | + Some(Deeplink::Asset { |
| 145 | + asset_id: AssetId::from_chain(Chain::Bitcoin) |
| 146 | + }) |
| 147 | + ); |
| 148 | + assert_eq!( |
| 149 | + Deeplink::from_url("https://gemwallet.com/tokens/ethereum/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), |
| 150 | + Some(Deeplink::Asset { |
| 151 | + asset_id: AssetId::token(Chain::Ethereum, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), |
| 152 | + }) |
| 153 | + ); |
| 154 | + assert_eq!( |
| 155 | + Deeplink::from_url("gem://tokens/bitcoin"), |
| 156 | + Some(Deeplink::Asset { |
| 157 | + asset_id: AssetId::from_chain(Chain::Bitcoin) |
| 158 | + }) |
| 159 | + ); |
| 160 | + assert_eq!(Deeplink::from_url("https://gemwallet.com/perpetuals"), Some(Deeplink::Perpetuals)); |
| 161 | + assert_eq!(Deeplink::from_url("gem://perpetuals"), Some(Deeplink::Perpetuals)); |
| 162 | + assert_eq!( |
| 163 | + Deeplink::from_url("https://gemwallet.com/rewards?code=gemcoder"), |
| 164 | + Some(Deeplink::Rewards { |
| 165 | + code: Some("gemcoder".to_string()), |
| 166 | + }) |
| 167 | + ); |
| 168 | + assert_eq!( |
| 169 | + Deeplink::from_url("https://gemwallet.com/join/gemcoder"), |
| 170 | + Some(Deeplink::Rewards { |
| 171 | + code: Some("gemcoder".to_string()), |
| 172 | + }) |
| 173 | + ); |
| 174 | + assert_eq!(Deeplink::from_url("https://gemwallet.com/join"), Some(Deeplink::Rewards { code: None })); |
| 175 | + assert_eq!(Deeplink::from_url("https://gemwallet.com/tokens"), None); |
| 176 | + assert_eq!(Deeplink::from_url("https://gemwallet.com/tokens/notachain"), None); |
| 177 | + assert_eq!(Deeplink::from_url("https://example.com/tokens/bitcoin"), None); |
| 178 | + assert_eq!(Deeplink::from_url("https://gemwallet.com/unknown"), None); |
| 179 | + assert_eq!(Deeplink::from_url("not a url"), None); |
16 | 180 | } |
17 | 181 | } |
0 commit comments