diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d7a76f2673..56fd0d15eb 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -34,6 +34,7 @@ jobs: runs-on: ${{ matrix.os }} env: CARGO_TERM_COLOR: always + IPINFO_API_TOKEN: ${{ secrets.IPINFO_API_TOKEN }} steps: - name: Install Dependencies (Linux) run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler diff --git a/Cargo.lock b/Cargo.lock index ff0b3f7cd2..468c8ce88a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6062,7 +6062,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "1.0.0-rc.5" +version = "1.0.0-rc.6" dependencies = [ "anyhow", "axum 0.7.7", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 1698cbb858..3408d1e4ef 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "1.0.0-rc.5" +version = "1.0.0-rc.6" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs index 1042b15cce..369acee1a7 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Addr, Coin}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; pub(crate) struct IpInfoClient { client: reqwest::Client, @@ -15,11 +15,7 @@ impl IpInfoClient { } pub(crate) async fn locate_ip(&self, ip: impl AsRef) -> anyhow::Result { - let url = format!( - "https://ipinfo.io/{}/country?token={}", - ip.as_ref(), - &self.token - ); + let url = format!("https://ipinfo.io/{}?token={}", ip.as_ref(), &self.token); let response = self .client .get(url) @@ -33,11 +29,12 @@ impl IpInfoClient { } anyhow::Error::from(err) })?; - let response_text = response.text().await?.trim().to_string(); + let raw_response = response.text().await?; + let response: LocationResponse = + serde_json::from_str(&raw_response).inspect_err(|e| tracing::error!("{e}"))?; + let location = response.into(); - Ok(Location { - two_letter_iso_country_code: response_text, - }) + Ok(location) } /// check DOESN'T consume bandwidth allowance @@ -64,7 +61,7 @@ impl IpInfoClient { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub(crate) struct NodeGeoData { pub(crate) identity_key: String, pub(crate) owner: Addr, @@ -72,15 +69,55 @@ pub(crate) struct NodeGeoData { pub(crate) location: Location, } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize)] pub(crate) struct Location { pub(crate) two_letter_iso_country_code: String, + #[serde(flatten)] + pub(crate) location: Coordinates, +} + +impl From for Location { + fn from(value: LocationResponse) -> Self { + Self { + two_letter_iso_country_code: value.two_letter_iso_country_code, + location: value.loc, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct LocationResponse { + #[serde(rename = "country")] + pub(crate) two_letter_iso_country_code: String, + #[serde(deserialize_with = "deserialize_loc")] + pub(crate) loc: Coordinates, +} + +fn deserialize_loc<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let loc_raw = String::deserialize(deserializer)?; + match loc_raw.split_once(',') { + Some((lat, long)) => Ok(Coordinates { + latitude: lat.parse().map_err(serde::de::Error::custom)?, + longitude: long.parse().map_err(serde::de::Error::custom)?, + }), + None => Err(serde::de::Error::custom("coordinates")), + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub(crate) struct Coordinates { + pub(crate) latitude: f64, + pub(crate) longitude: f64, } impl Location { pub(crate) fn empty() -> Self { Self { two_letter_iso_country_code: String::new(), + location: Coordinates::default(), } } } @@ -110,3 +147,38 @@ pub(crate) mod ipinfo { pub(crate) remaining: u64, } } + +#[cfg(test)] +mod api_regression { + + use super::*; + use std::{env::var, sync::LazyLock}; + + static IPINFO_TOKEN: LazyLock = LazyLock::new(|| var("IPINFO_API_TOKEN").unwrap()); + + #[tokio::test] + async fn should_parse_response() { + let client = IpInfoClient::new(&(*IPINFO_TOKEN)); + let my_ip = reqwest::get("https://api.ipify.org") + .await + .expect("Couldn't get own IP") + .text() + .await + .unwrap(); + + let location_result = client.locate_ip(my_ip).await; + assert!(location_result.is_ok(), "Did ipinfo response change?"); + + assert!( + client.check_remaining_bandwidth().await.is_ok(), + "Failed to check remaining bandwidth?" + ); + + // when serialized, these fields should be present because they're exposed over API + let location_result = location_result.unwrap(); + let json = serde_json::to_value(&location_result).unwrap(); + assert!(json.get("two_letter_iso_country_code").is_some()); + assert!(json.get("latitude").is_some()); + assert!(json.get("longitude").is_some()); + } +}