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 bech32::{Bech32m, Hrp};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::base32::{FEDIMINT_PREFIX, PrefixedDecodeError};
use crate::config::FederationId;
use crate::encoding::{Decodable, DecodeError, Encodable};
use crate::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
use crate::util::{FmtCompact as _, 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 = InviteCodeParseError;
fn from_str(encoded: &str) -> Result<Self, Self::Err> {
if encoded
.get(..FEDIMINT_PREFIX.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(FEDIMINT_PREFIX))
{
return Ok(crate::base32::decode_prefixed(FEDIMINT_PREFIX, encoded)?);
}
let (hrp, data) = bech32::decode(encoded)?;
if hrp != BECH32_HRP {
return Err(InviteCodeParseError::InvalidHrp { hrp });
}
let invite = Self::consensus_decode_whole(&data, &ModuleRegistry::default())?;
Ok(invite)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum InviteCodeParseError {
#[error("Invalid bech32 encoding: {0}")]
Bech32(#[from] bech32::DecodeError),
#[error(
"Invalid bech32 human-readable part '{hrp}', expected '{}'",
BECH32_HRP
)]
InvalidHrp { hrp: bech32::Hrp },
#[error("Invalid invite code payload: {}", .0.fmt_compact())]
Decode(DecodeError),
#[error("Invalid prefixed base32 invite code: {}", .0.fmt_compact())]
Base32(PrefixedDecodeError),
}
impl From<DecodeError> for InviteCodeParseError {
fn from(source: DecodeError) -> Self {
Self::Decode(source)
}
}
impl From<PrefixedDecodeError> for InviteCodeParseError {
fn from(source: PrefixedDecodeError) -> Self {
Self::Base32(source)
}
}
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;