use core::fmt;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
use std::io::Read;
use std::str::FromStr;
use anyhow::ensure;
use bech32::{Bech32m, Hrp};
use serde::{Deserialize, Serialize};
use crate::base32::FEDIMINT_PREFIX;
use crate::config::FederationId;
use crate::encoding::{Decodable, DecodeError, Encodable};
use crate::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
use crate::util::SafeUrl;
use crate::{NumPeersExt, PeerId};
#[derive(Clone, Debug, Eq, PartialEq, Encodable, Hash, Ord, PartialOrd)]
pub struct InviteCode(Vec<InviteCodePart>);
impl Decodable for InviteCode {
fn consensus_decode_partial<R: Read>(
r: &mut R,
modules: &ModuleDecoderRegistry,
) -> Result<Self, DecodeError> {
let inner: Vec<InviteCodePart> = Decodable::consensus_decode_partial(r, modules)?;
if !inner
.iter()
.any(|data| matches!(data, InviteCodePart::Api { .. }))
{
return Err(DecodeError::from_str(
"No API was provided in the invite code",
));
}
if !inner
.iter()
.any(|data| matches!(data, InviteCodePart::FederationId(_)))
{
return Err(DecodeError::from_str(
"No Federation ID provided in invite code",
));
}
Ok(Self(inner))
}
}
impl InviteCode {
pub fn new(
url: SafeUrl,
peer: PeerId,
federation_id: FederationId,
api_secret: Option<String>,
) -> Self {
let mut s = Self(vec![
InviteCodePart::Api { url, peer },
InviteCodePart::FederationId(federation_id),
]);
if let Some(api_secret) = api_secret {
s.0.push(InviteCodePart::ApiSecret(api_secret));
}
s
}
pub fn from_map(
peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
federation_id: FederationId,
api_secret: Option<String>,
) -> Self {
let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
.iter()
.take(max_size)
.map(|(peer, url)| InviteCodePart::Api {
url: url.clone(),
peer: *peer,
})
.collect();
code_vec.push(InviteCodePart::FederationId(federation_id));
if let Some(api_secret) = api_secret {
code_vec.push(InviteCodePart::ApiSecret(api_secret));
}
Self(code_vec)
}
pub fn new_with_essential_num_guardians(
peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
federation_id: FederationId,
) -> Self {
let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
.iter()
.take(max_size)
.map(|(peer, url)| InviteCodePart::Api {
url: url.clone(),
peer: *peer,
})
.collect();
code_vec.push(InviteCodePart::FederationId(federation_id));
Self(code_vec)
}
pub fn url(&self) -> SafeUrl {
self.0
.iter()
.find_map(|data| match data {
InviteCodePart::Api { url, .. } => Some(url.clone()),
_ => None,
})
.expect("Ensured by constructor")
}
pub fn api_secret(&self) -> Option<String> {
self.0.iter().find_map(|data| match data {
InviteCodePart::ApiSecret(api_secret) => Some(api_secret.clone()),
_ => None,
})
}
pub fn peer(&self) -> PeerId {
self.0
.iter()
.find_map(|data| match data {
InviteCodePart::Api { peer, .. } => Some(*peer),
_ => None,
})
.expect("Ensured by constructor")
}
pub fn peers(&self) -> BTreeMap<PeerId, SafeUrl> {
self.0
.iter()
.filter_map(|entry| match entry {
InviteCodePart::Api { url, peer } => Some((*peer, url.clone())),
_ => None,
})
.collect()
}
pub fn federation_id(&self) -> FederationId {
self.0
.iter()
.find_map(|data| match data {
InviteCodePart::FederationId(federation_id) => Some(*federation_id),
_ => None,
})
.expect("Ensured by constructor")
}
}
#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable, Hash, Ord, PartialOrd)]
enum InviteCodePart {
Api {
url: SafeUrl,
peer: PeerId,
},
FederationId(FederationId),
ApiSecret(String),
#[encodable_default]
Default { variant: u64, bytes: Vec<u8> },
}
const BECH32_HRP: Hrp = Hrp::parse_unchecked("fed1");
impl FromStr for InviteCode {
type Err = anyhow::Error;
fn from_str(encoded: &str) -> Result<Self, Self::Err> {
if let Ok(invite_code) = crate::base32::decode_prefixed(FEDIMINT_PREFIX, encoded) {
return Ok(invite_code);
}
let (hrp, data) = bech32::decode(encoded)?;
ensure!(hrp == BECH32_HRP, "Invalid HRP in bech32 encoding");
let invite = Self::consensus_decode_whole(&data, &ModuleRegistry::default())?;
Ok(invite)
}
}
impl Display for InviteCode {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
let data = self.consensus_encode_to_vec();
let encode = bech32::encode::<Bech32m>(BECH32_HRP, &data).map_err(|_| fmt::Error)?;
formatter.write_str(&encode)
}
}
impl Serialize for InviteCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
String::serialize(&self.to_string(), serializer)
}
}
impl<'de> Deserialize<'de> for InviteCode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let string = Cow::<str>::deserialize(deserializer)?;
Self::from_str(&string).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use fedimint_core::PeerId;
use fedimint_core::base32::FEDIMINT_PREFIX;
use crate::config::FederationId;
use crate::invite_code::InviteCode;
#[test]
fn test_invite_code_to_from_string() {
let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
let invite_code = InviteCode::from_str(invite_code_str).expect("valid invite code");
InviteCode::from_str(&crate::base32::encode_prefixed(
FEDIMINT_PREFIX,
&invite_code,
))
.expect("Failed to parse base 32 invite code");
assert_eq!(invite_code.to_string(), invite_code_str);
assert_eq!(
invite_code.0,
[
crate::invite_code::InviteCodePart::Api {
url: "wss://fedimintd.mplsfed.foo/".parse().expect("valid url"),
peer: PeerId::new(0),
},
crate::invite_code::InviteCodePart::FederationId(FederationId(
bitcoin::hashes::sha256::Hash::from_str(
"bea7ff4116f2b1d324c7b5d699cce4ac7408cee41db2c88027e21b76fff3b9f4"
)
.expect("valid hash")
))
]
);
}
}