11use cosmwasm_std:: { Addr , Coin } ;
2- use serde:: { Deserialize , Serialize } ;
2+ use serde:: { Deserialize , Deserializer , Serialize } ;
33
44pub ( crate ) struct IpInfoClient {
55 client : reqwest:: Client ,
@@ -15,11 +15,7 @@ impl IpInfoClient {
1515 }
1616
1717 pub ( crate ) async fn locate_ip ( & self , ip : impl AsRef < str > ) -> anyhow:: Result < Location > {
18- let url = format ! (
19- "https://ipinfo.io/{}/country?token={}" ,
20- ip. as_ref( ) ,
21- & self . token
22- ) ;
18+ let url = format ! ( "https://ipinfo.io/{}?token={}" , ip. as_ref( ) , & self . token) ;
2319 let response = self
2420 . client
2521 . get ( url)
@@ -33,11 +29,12 @@ impl IpInfoClient {
3329 }
3430 anyhow:: Error :: from ( err)
3531 } ) ?;
36- let response_text = response. text ( ) . await ?. trim ( ) . to_string ( ) ;
32+ let raw_response = response. text ( ) . await ?;
33+ let response: LocationResponse =
34+ serde_json:: from_str ( & raw_response) . inspect_err ( |e| tracing:: error!( "{e}" ) ) ?;
35+ let location = response. into ( ) ;
3736
38- Ok ( Location {
39- two_letter_iso_country_code : response_text,
40- } )
37+ Ok ( location)
4138 }
4239
4340 /// check DOESN'T consume bandwidth allowance
@@ -64,23 +61,63 @@ impl IpInfoClient {
6461 }
6562}
6663
67- #[ derive( Debug , Clone , Serialize , Deserialize ) ]
64+ #[ derive( Debug , Clone , Serialize ) ]
6865pub ( crate ) struct NodeGeoData {
6966 pub ( crate ) identity_key : String ,
7067 pub ( crate ) owner : Addr ,
7168 pub ( crate ) pledge_amount : Coin ,
7269 pub ( crate ) location : Location ,
7370}
7471
75- #[ derive( Debug , Clone , Default , Serialize , Deserialize ) ]
72+ #[ derive( Debug , Clone , Default , Serialize ) ]
7673pub ( crate ) struct Location {
7774 pub ( crate ) two_letter_iso_country_code : String ,
75+ #[ serde( flatten) ]
76+ pub ( crate ) location : Coordinates ,
77+ }
78+
79+ impl From < LocationResponse > for Location {
80+ fn from ( value : LocationResponse ) -> Self {
81+ Self {
82+ two_letter_iso_country_code : value. two_letter_iso_country_code ,
83+ location : value. loc ,
84+ }
85+ }
86+ }
87+
88+ #[ derive( Debug , Clone , Deserialize ) ]
89+ pub ( crate ) struct LocationResponse {
90+ #[ serde( rename = "country" ) ]
91+ pub ( crate ) two_letter_iso_country_code : String ,
92+ #[ serde( deserialize_with = "deserialize_loc" ) ]
93+ pub ( crate ) loc : Coordinates ,
94+ }
95+
96+ fn deserialize_loc < ' de , D > ( deserializer : D ) -> Result < Coordinates , D :: Error >
97+ where
98+ D : Deserializer < ' de > ,
99+ {
100+ let loc_raw = String :: deserialize ( deserializer) ?;
101+ match loc_raw. split_once ( ',' ) {
102+ Some ( ( lat, long) ) => Ok ( Coordinates {
103+ latitude : lat. parse ( ) . map_err ( serde:: de:: Error :: custom) ?,
104+ longitude : long. parse ( ) . map_err ( serde:: de:: Error :: custom) ?,
105+ } ) ,
106+ None => Err ( serde:: de:: Error :: custom ( "coordinates" ) ) ,
107+ }
108+ }
109+
110+ #[ derive( Debug , Default , Clone , Serialize , Deserialize ) ]
111+ pub ( crate ) struct Coordinates {
112+ pub ( crate ) latitude : f64 ,
113+ pub ( crate ) longitude : f64 ,
78114}
79115
80116impl Location {
81117 pub ( crate ) fn empty ( ) -> Self {
82118 Self {
83119 two_letter_iso_country_code : String :: new ( ) ,
120+ location : Coordinates :: default ( ) ,
84121 }
85122 }
86123}
@@ -110,3 +147,38 @@ pub(crate) mod ipinfo {
110147 pub ( crate ) remaining : u64 ,
111148 }
112149}
150+
151+ #[ cfg( test) ]
152+ mod api_regression {
153+
154+ use super :: * ;
155+ use std:: { env:: var, sync:: LazyLock } ;
156+
157+ static IPINFO_TOKEN : LazyLock < String > = LazyLock :: new ( || var ( "IPINFO_API_TOKEN" ) . unwrap ( ) ) ;
158+
159+ #[ tokio:: test]
160+ async fn should_parse_response ( ) {
161+ let client = IpInfoClient :: new ( & ( * IPINFO_TOKEN ) ) ;
162+ let my_ip = reqwest:: get ( "https://api.ipify.org" )
163+ . await
164+ . expect ( "Couldn't get own IP" )
165+ . text ( )
166+ . await
167+ . unwrap ( ) ;
168+
169+ let location_result = client. locate_ip ( my_ip) . await ;
170+ assert ! ( location_result. is_ok( ) , "Did ipinfo response change?" ) ;
171+
172+ assert ! (
173+ client. check_remaining_bandwidth( ) . await . is_ok( ) ,
174+ "Failed to check remaining bandwidth?"
175+ ) ;
176+
177+ // when serialized, these fields should be present because they're exposed over API
178+ let location_result = location_result. unwrap ( ) ;
179+ let json = serde_json:: to_value ( & location_result) . unwrap ( ) ;
180+ assert ! ( json. get( "two_letter_iso_country_code" ) . is_some( ) ) ;
181+ assert ! ( json. get( "latitude" ) . is_some( ) ) ;
182+ assert ! ( json. get( "longitude" ) . is_some( ) ) ;
183+ }
184+ }
0 commit comments