use std::str::FromStr;
use iroh::address_lookup::UserData;
use iroh::endpoint_info::MaxLengthExceededError;
use p2panda_core::{
IdentityError, Signature,
timestamp::{HybridTimestamp, HybridTimestampError},
};
use thiserror::Error;
use crate::addrs::{AuthenticatedTransportInfo, NodeTransportInfo, TransportAddress};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UserDataTransportInfo {
pub signature: Signature,
pub timestamp: HybridTimestamp,
pub relay_url: Option<iroh::RelayUrl>,
}
impl UserDataTransportInfo {
pub fn from_transport_info(info: AuthenticatedTransportInfo) -> Self {
Self {
signature: info.signature,
timestamp: info.timestamp,
relay_url: info.addresses().iter().find_map(|addr| match addr {
TransportAddress::Iroh(addr) => addr.relay_urls().next().cloned(),
}),
}
}
}
impl TryFrom<AuthenticatedTransportInfo> for UserData {
type Error = MaxLengthExceededError;
fn try_from(info: AuthenticatedTransportInfo) -> Result<Self, Self::Error> {
UserData::try_from(UserDataTransportInfo::from_transport_info(info))
}
}
const INFO_SEPARATOR: char = '|';
impl TryFrom<UserDataTransportInfo> for UserData {
type Error = MaxLengthExceededError;
fn try_from(info: UserDataTransportInfo) -> Result<Self, Self::Error> {
UserData::try_from(format!(
"{}{INFO_SEPARATOR}{}{}",
info.signature,
info.timestamp,
info.relay_url
.map(|url| format!("{INFO_SEPARATOR}{url}"))
.unwrap_or_default()
))
}
}
impl TryFrom<UserData> for UserDataTransportInfo {
type Error = UserDataInfoError;
fn try_from(user_data: UserData) -> Result<Self, Self::Error> {
let user_data = user_data.to_string();
let parts: Vec<_> = user_data.split(INFO_SEPARATOR).collect();
if parts.len() != 2 && parts.len() != 3 {
return Err(UserDataInfoError::Size(parts.len()));
}
let mut parts = parts.iter();
let signature_str = parts.next().expect("we've checked the size before");
let timestamp_str = parts.next().expect("we've checked the size before");
let signature = Signature::from_str(signature_str)?;
let timestamp = HybridTimestamp::from_str(timestamp_str)?;
let relay_url = if let Some(relay_url_str) = parts.next() {
Some(iroh::RelayUrl::from_str(relay_url_str)?)
} else {
None
};
Ok(Self {
signature,
timestamp,
relay_url,
})
}
}
#[derive(Debug, Error)]
pub enum UserDataInfoError {
#[error("invalid size of separated info parts, expected 2-3, given: {0}")]
Size(usize),
#[error(transparent)]
Signature(#[from] IdentityError),
#[error(transparent)]
Timestamp(#[from] HybridTimestampError),
#[error(transparent)]
RelayUrl(#[from] iroh::RelayUrlParseError),
}
#[cfg(test)]
mod tests {
use iroh::address_lookup::UserData;
use p2panda_core::SigningKey;
use crate::utils::from_verifying_key;
use super::{AuthenticatedTransportInfo, UserDataTransportInfo};
#[test]
fn transport_info_to_user_data() {
let signing_key = SigningKey::generate();
let transport_info = AuthenticatedTransportInfo::new_unsigned()
.sign(&signing_key)
.unwrap();
let txt_info = UserDataTransportInfo::from_transport_info(transport_info);
let user_data = UserData::try_from(txt_info.clone()).unwrap();
let txt_info_again = UserDataTransportInfo::try_from(user_data).unwrap();
assert_eq!(txt_info, txt_info_again);
}
#[test]
fn transport_info_to_user_data_with_relay_url() {
let signing_key = SigningKey::generate();
let mut transport_info = AuthenticatedTransportInfo::new_unsigned();
transport_info.add_addr(
iroh::EndpointAddr::new(from_verifying_key(signing_key.verifying_key()))
.with_ip_addr("127.0.0.1:8080".parse().unwrap())
.with_relay_url(
"https://euc1-1.relay.n0.iroh-canary.iroh.link./"
.parse()
.unwrap(),
)
.into(),
);
let transport_info = transport_info.sign(&signing_key).unwrap();
let txt_info = UserDataTransportInfo::from_transport_info(transport_info);
let user_data = UserData::try_from(txt_info.clone()).unwrap();
let txt_info_again = UserDataTransportInfo::try_from(user_data).unwrap();
assert_eq!(txt_info, txt_info_again);
}
}