#[macro_use]
extern crate serde_derive;
use http::uri::Uri;
use hyper::Client;
use hyper_tls::HttpsConnector;
use std::result::Result;
use stellar_base::PublicKey;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StellarToml {
#[serde(alias = "VERSION")]
pub version: Option<String>,
#[serde(alias = "NETWORK_PASSPHRASE")]
pub network_passphrase: Option<String>,
#[serde(alias = "FEDERATION_SERVER")]
#[serde(default, with = "option_display_fromstr")]
pub federation_server: Option<Uri>,
#[serde(alias = "AUTH_SERVER")]
#[serde(default, with = "option_display_fromstr")]
pub auth_server: Option<Uri>,
#[serde(alias = "TRANSFER_SERVER")]
#[serde(default, with = "option_display_fromstr")]
pub transfer_server: Option<Uri>,
#[serde(alias = "TRANSFER_SERVER_SEP0024")]
#[serde(default, with = "option_display_fromstr")]
pub transfer_server_sep0024: Option<Uri>,
#[serde(alias = "KYC_SERVER")]
#[serde(default, with = "option_display_fromstr")]
pub kyc_server: Option<Uri>,
#[serde(alias = "WEB_AUTH_ENDPOINT")]
#[serde(default, with = "option_display_fromstr")]
pub web_auth_endpoint: Option<Uri>,
#[serde(alias = "SIGNING_KEY")]
#[serde(default, with = "option_display_fromstr")]
pub signing_key: Option<PublicKey>,
#[serde(alias = "HORIZON_URL")]
#[serde(default, with = "option_display_fromstr")]
pub horizon_url: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "ACCOUNTS")]
pub accounts: Vec<String>,
#[serde(alias = "URI_REQUEST_SIGNING_KEY")]
pub uri_request_signing_key: Option<String>,
#[serde(alias = "DOCUMENTATION")]
pub documentation: Option<Documentation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "PRINCIPALS")]
pub principals: Vec<PointOfContact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "CURRENCIES")]
pub currencies: Vec<Currency>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "VALIDATORS")]
pub validators: Vec<Validator>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Documentation {
#[serde(alias = "ORG_NAME")]
pub org_name: Option<String>,
#[serde(alias = "ORG_DBA")]
pub org_dba: Option<String>,
#[serde(alias = "ORG_URL")]
#[serde(default, with = "option_display_fromstr")]
pub org_url: Option<Uri>,
#[serde(alias = "ORG_LOGO")]
#[serde(default, with = "option_display_fromstr")]
pub org_logo: Option<Uri>,
#[serde(alias = "ORG_DESCRIPTION")]
pub org_description: Option<String>,
#[serde(alias = "ORG_PHYSICAL_ADDRESS")]
pub org_physical_address: Option<String>,
#[serde(alias = "ORG_PHYSICAL_ADDRESS_ATTESTATION")]
#[serde(default, with = "option_display_fromstr")]
pub org_physical_address_attestation: Option<Uri>,
#[serde(alias = "ORG_PHONE_NUMBER")]
pub org_phone_number: Option<String>,
#[serde(alias = "ORG_PHONE_NUMBER_ATTESTIATION")]
#[serde(default, with = "option_display_fromstr")]
pub org_phone_number_attestation: Option<Uri>,
#[serde(alias = "ORG_KEYBASE")]
pub org_keybase: Option<String>,
#[serde(alias = "ORG_TWITTER")]
pub org_twitter: Option<String>,
#[serde(alias = "ORG_GITHUB")]
pub org_github: Option<String>,
#[serde(alias = "ORG_OFFICIAL_EMAIL")]
pub org_official_email: Option<String>,
#[serde(alias = "ORG_LICENSING_AUTHORITY")]
pub org_licensing_authority: Option<String>,
#[serde(alias = "ORG_LICENSE_TYPE")]
pub org_license_type: Option<String>,
#[serde(alias = "ORG_LICENSE_NUMBER")]
pub org_license_number: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PointOfContact {
#[serde(alias = "NAME")]
pub name: Option<String>,
#[serde(alias = "EMAIL")]
pub email: Option<String>,
#[serde(alias = "KEYBASE")]
pub keybase: Option<String>,
#[serde(alias = "TELEGRAM")]
pub telegram: Option<String>,
#[serde(alias = "TWITTER")]
pub twitter: Option<String>,
#[serde(alias = "GITHUB")]
pub github: Option<String>,
#[serde(alias = "ID_PHOTO_HASH")]
pub id_photo_hash: Option<String>,
#[serde(alias = "VERIFICATION_PHOTO_HASH")]
pub verification_photo_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CurrencyStatus {
#[serde(alias = "live")]
Live,
#[serde(alias = "dead")]
Dead,
#[serde(alias = "test")]
Test,
#[serde(alias = "private")]
Private,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnchoredCurrencyType {
#[serde(alias = "fiat")]
Fiat,
#[serde(alias = "crypto")]
Crypto,
#[serde(alias = "stock")]
Stock,
#[serde(alias = "bond")]
Bond,
#[serde(alias = "commodity")]
Commodity,
#[serde(alias = "realestate")]
RealEstate,
#[serde(alias = "other")]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Currency {
#[serde(alias = "CODE")]
pub code: Option<String>,
#[serde(alias = "CODE_TEMPLATE")]
pub code_template: Option<String>,
#[serde(alias = "ISSUER")]
#[serde(with = "option_display_fromstr")]
pub issuer: Option<PublicKey>,
#[serde(alias = "STATUS")]
pub status: Option<CurrencyStatus>,
#[serde(alias = "DISPLAY_DECIMALS")]
pub display_decimals: Option<u8>,
#[serde(alias = "NAME")]
pub name: Option<String>,
#[serde(rename = "desc")]
#[serde(alias = "DESC")]
pub description: Option<String>,
#[serde(alias = "CONDITIONS")]
pub conditions: Option<String>,
#[serde(alias = "IMAGE")]
pub image: Option<String>,
#[serde(alias = "FIXED_NUMBER")]
pub fixed_number: Option<i64>,
#[serde(alias = "MAX_NUMBER")]
pub max_number: Option<i64>,
#[serde(alias = "IS_UNLIMITED")]
pub is_unlimited: Option<bool>,
#[serde(alias = "IS_ASSET_ANCHORED")]
pub is_asset_anchored: Option<bool>,
#[serde(alias = "ANCHOR_ASSET_TYPE")]
pub anchor_asset_type: Option<AnchoredCurrencyType>,
#[serde(alias = "ANCHOR_ASSET")]
pub anchor_asset: Option<String>,
#[serde(alias = "REDEMPTION_INSTRUCTIONS")]
pub redemption_instructions: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "COLLATERAL_ADDRESSES")]
pub collateral_addresses: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "COLLATERAL_ADDRESS_MESSAGES")]
pub collateral_address_messages: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "COLLATERAL_ADDRESS_SIGNATURES")]
pub collateral_address_signatures: Vec<String>,
#[serde(alias = "REGULATED")]
pub regulated: Option<bool>,
#[serde(alias = "APPROVAL_SERVER")]
#[serde(default, with = "option_display_fromstr")]
pub approval_server: Option<Uri>,
#[serde(alias = "APPROVAL_CRITERIA")]
pub approval_criteria: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Validator {
#[serde(alias = "ALIAS")]
pub alias: Option<String>,
#[serde(alias = "DISPLAY_NAME")]
pub display_name: Option<String>,
#[serde(alias = "PUBLIC_KEY")]
#[serde(default, with = "option_display_fromstr")]
pub public_key: Option<PublicKey>,
#[serde(alias = "HOST")]
pub host: Option<String>,
#[serde(alias = "HISTORY")]
#[serde(default, with = "option_display_fromstr")]
pub history: Option<Uri>,
}
pub const STELLAR_TOML_PATH: &str = ".well-known/stellar.toml";
pub async fn resolve(domain: &str) -> Result<StellarToml, Error> {
let url = stellar_toml_path(&domain)?;
resolve_url(&url).await
}
pub async fn resolve_url(url: &Uri) -> Result<StellarToml, Error> {
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);
let uri: hyper::Uri = url.to_string().parse()?;
let response = client.get(uri).await?;
if response.status().is_success() {
let bytes = hyper::body::to_bytes(response).await?;
let result: StellarToml = toml::from_slice(&bytes)?;
Ok(result)
} else if response.status().is_client_error() {
Err(Error::ClientError(response))
} else {
Err(Error::ServerError(response))
}
}
pub fn stellar_toml_path(domain: &str) -> Result<Uri, Error> {
let url = format!("https://{}/{}", domain, STELLAR_TOML_PATH);
Ok(url.parse()?)
}
pub fn stellar_toml_path_insecure(domain: &str) -> Result<Uri, Error> {
let url = format!("http://{}/{}", domain, STELLAR_TOML_PATH);
Ok(url.parse()?)
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("client response error")]
ClientError(hyper::Response<hyper::Body>),
#[error("server response error")]
ServerError(hyper::Response<hyper::Body>),
#[error("toml parse error")]
TomlParseError(#[from] toml::de::Error),
#[error("hyper error")]
HyperError(#[from] hyper::Error),
#[error("invalid uri")]
InvalidUri(#[from] http::uri::InvalidUri),
}
mod option_display_fromstr {
use serde::{
de::{Deserialize, Deserializer},
ser::Serializer,
};
use serde_with::rust::display_fromstr;
use std::{fmt::Display, str::FromStr};
#[derive(Deserialize)]
struct DeWrapper<T>(#[serde(with = "display_fromstr")] T)
where
T: FromStr,
T::Err: Display;
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: Display,
{
let v: Option<DeWrapper<T>> = Option::deserialize(deserializer)?;
Ok(v.map(|DeWrapper(a)| a))
}
pub fn serialize<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: Display,
{
match value {
None => serializer.serialize_unit(),
Some(v) => serializer.serialize_str(&*v.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stellar_toml_path() {
let url = stellar_toml_path("foo.bar.example.org").unwrap();
assert_eq!(
"https://foo.bar.example.org/.well-known/stellar.toml",
url.to_string()
);
}
#[test]
fn test_stellar_toml_path_insecure() {
let url = stellar_toml_path_insecure("foo.bar.example.org").unwrap();
assert_eq!(
"http://foo.bar.example.org/.well-known/stellar.toml",
url.to_string()
);
}
#[tokio::test]
async fn test_resolve_known_stellar_tomls() {
let stellar = resolve("www.stellar.org").await.unwrap();
assert!(!stellar.validators.is_empty());
let usd = resolve("stablecoin.anchorusd.com").await.unwrap();
assert!(!usd.currencies.is_empty());
}
}