1use core::fmt;
2use std::borrow::Cow;
3use std::collections::BTreeMap;
4use std::fmt::{Display, Formatter};
5use std::io::Read;
6use std::str::FromStr;
7
8use bech32::{Bech32m, Hrp};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::base32::{FEDIMINT_PREFIX, PrefixedDecodeError};
13use crate::config::FederationId;
14use crate::encoding::{Decodable, DecodeError, Encodable};
15use crate::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
16use crate::util::{FmtCompact as _, SafeUrl};
17use crate::{NumPeersExt, PeerId};
18
19#[derive(Clone, Debug, Eq, PartialEq, Encodable, Hash, Ord, PartialOrd)]
28pub struct InviteCode(Vec<InviteCodePart>);
29
30#[cfg(feature = "uniffi")]
31uniffi::custom_type!(InviteCode, String, {
32 lower: |i| i.to_string(),
33 try_lift: |s| s.parse::<InviteCode>().map_err(anyhow::Error::from),
34});
35
36impl Decodable for InviteCode {
37 fn consensus_decode_partial<R: Read>(
38 r: &mut R,
39 modules: &ModuleDecoderRegistry,
40 ) -> Result<Self, DecodeError> {
41 let inner: Vec<InviteCodePart> = Decodable::consensus_decode_partial(r, modules)?;
42
43 if !inner
44 .iter()
45 .any(|data| matches!(data, InviteCodePart::Api { .. }))
46 {
47 return Err(DecodeError::from_str(
48 "No API was provided in the invite code",
49 ));
50 }
51
52 if !inner
53 .iter()
54 .any(|data| matches!(data, InviteCodePart::FederationId(_)))
55 {
56 return Err(DecodeError::from_str(
57 "No Federation ID provided in invite code",
58 ));
59 }
60
61 Ok(Self(inner))
62 }
63}
64
65impl InviteCode {
66 pub fn new(
67 url: SafeUrl,
68 peer: PeerId,
69 federation_id: FederationId,
70 api_secret: Option<String>,
71 ) -> Self {
72 let mut s = Self(vec![
73 InviteCodePart::Api { url, peer },
74 InviteCodePart::FederationId(federation_id),
75 ]);
76
77 if let Some(api_secret) = api_secret {
78 s.0.push(InviteCodePart::ApiSecret(api_secret));
79 }
80
81 s
82 }
83
84 pub fn from_map(
85 peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
86 federation_id: FederationId,
87 api_secret: Option<String>,
88 ) -> Self {
89 let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
90 let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
91 .iter()
92 .take(max_size)
93 .map(|(peer, url)| InviteCodePart::Api {
94 url: url.clone(),
95 peer: *peer,
96 })
97 .collect();
98
99 code_vec.push(InviteCodePart::FederationId(federation_id));
100
101 if let Some(api_secret) = api_secret {
102 code_vec.push(InviteCodePart::ApiSecret(api_secret));
103 }
104
105 Self(code_vec)
106 }
107
108 pub fn new_with_essential_num_guardians(
111 peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
112 federation_id: FederationId,
113 ) -> Self {
114 let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
115 let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
116 .iter()
117 .take(max_size)
118 .map(|(peer, url)| InviteCodePart::Api {
119 url: url.clone(),
120 peer: *peer,
121 })
122 .collect();
123 code_vec.push(InviteCodePart::FederationId(federation_id));
124
125 Self(code_vec)
126 }
127
128 pub fn url(&self) -> SafeUrl {
130 self.0
131 .iter()
132 .find_map(|data| match data {
133 InviteCodePart::Api { url, .. } => Some(url.clone()),
134 _ => None,
135 })
136 .expect("Ensured by constructor")
137 }
138
139 pub fn api_secret(&self) -> Option<String> {
141 self.0.iter().find_map(|data| match data {
142 InviteCodePart::ApiSecret(api_secret) => Some(api_secret.clone()),
143 _ => None,
144 })
145 }
146 pub fn peer(&self) -> PeerId {
149 self.0
150 .iter()
151 .find_map(|data| match data {
152 InviteCodePart::Api { peer, .. } => Some(*peer),
153 _ => None,
154 })
155 .expect("Ensured by constructor")
156 }
157
158 pub fn peers(&self) -> BTreeMap<PeerId, SafeUrl> {
160 self.0
161 .iter()
162 .filter_map(|entry| match entry {
163 InviteCodePart::Api { url, peer } => Some((*peer, url.clone())),
164 _ => None,
165 })
166 .collect()
167 }
168
169 pub fn federation_id(&self) -> FederationId {
172 self.0
173 .iter()
174 .find_map(|data| match data {
175 InviteCodePart::FederationId(federation_id) => Some(*federation_id),
176 _ => None,
177 })
178 .expect("Ensured by constructor")
179 }
180}
181
182#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable, Hash, Ord, PartialOrd)]
191enum InviteCodePart {
192 Api {
194 url: SafeUrl,
196 peer: PeerId,
198 },
199
200 FederationId(FederationId),
202
203 ApiSecret(String),
205
206 #[encodable_default]
208 Default { variant: u64, bytes: Vec<u8> },
209}
210
211const BECH32_HRP: Hrp = Hrp::parse_unchecked("fed1");
219
220impl FromStr for InviteCode {
221 type Err = InviteCodeParseError;
222
223 fn from_str(encoded: &str) -> Result<Self, Self::Err> {
224 if encoded
227 .get(..FEDIMINT_PREFIX.len())
228 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(FEDIMINT_PREFIX))
229 {
230 return Ok(crate::base32::decode_prefixed(FEDIMINT_PREFIX, encoded)?);
231 }
232
233 let (hrp, data) = bech32::decode(encoded)?;
234
235 if hrp != BECH32_HRP {
236 return Err(InviteCodeParseError::InvalidHrp { hrp });
237 }
238
239 let invite = Self::consensus_decode_whole(&data, &ModuleRegistry::default())?;
240
241 Ok(invite)
242 }
243}
244
245#[derive(Debug, Error)]
247#[non_exhaustive]
248pub enum InviteCodeParseError {
249 #[error("Invalid bech32 encoding: {0}")]
251 Bech32(#[from] bech32::DecodeError),
252 #[error(
254 "Invalid bech32 human-readable part '{hrp}', expected '{}'",
255 BECH32_HRP
256 )]
257 InvalidHrp { hrp: bech32::Hrp },
258 #[error("Invalid invite code payload: {}", .0.fmt_compact())]
260 Decode(DecodeError),
261 #[error("Invalid prefixed base32 invite code: {}", .0.fmt_compact())]
264 Base32(PrefixedDecodeError),
265}
266
267impl From<DecodeError> for InviteCodeParseError {
268 fn from(source: DecodeError) -> Self {
269 Self::Decode(source)
270 }
271}
272
273impl From<PrefixedDecodeError> for InviteCodeParseError {
274 fn from(source: PrefixedDecodeError) -> Self {
275 Self::Base32(source)
276 }
277}
278
279impl Display for InviteCode {
281 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
282 let data = self.consensus_encode_to_vec();
283 let encode = bech32::encode::<Bech32m>(BECH32_HRP, &data).map_err(|_| fmt::Error)?;
284 formatter.write_str(&encode)
285 }
286}
287
288impl Serialize for InviteCode {
289 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
290 where
291 S: serde::Serializer,
292 {
293 String::serialize(&self.to_string(), serializer)
294 }
295}
296
297impl<'de> Deserialize<'de> for InviteCode {
298 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299 where
300 D: serde::Deserializer<'de>,
301 {
302 let string = Cow::<str>::deserialize(deserializer)?;
303 Self::from_str(&string).map_err(serde::de::Error::custom)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use std::str::FromStr;
310
311 use fedimint_core::PeerId;
312 use fedimint_core::base32::FEDIMINT_PREFIX;
313
314 use super::{BECH32_HRP, InviteCodeParseError};
315 use crate::config::FederationId;
316 use crate::invite_code::InviteCode;
317
318 #[test]
319 fn test_invite_code_to_from_string() {
320 let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
321 let invite_code = InviteCode::from_str(invite_code_str).expect("valid invite code");
322
323 InviteCode::from_str(&crate::base32::encode_prefixed(
324 FEDIMINT_PREFIX,
325 &invite_code,
326 ))
327 .expect("Failed to parse base 32 invite code");
328
329 assert_eq!(invite_code.to_string(), invite_code_str);
330 assert_eq!(
331 invite_code.0,
332 [
333 crate::invite_code::InviteCodePart::Api {
334 url: "wss://fedimintd.mplsfed.foo/".parse().expect("valid url"),
335 peer: PeerId::new(0),
336 },
337 crate::invite_code::InviteCodePart::FederationId(FederationId(
338 bitcoin::hashes::sha256::Hash::from_str(
339 "bea7ff4116f2b1d324c7b5d699cce4ac7408cee41db2c88027e21b76fff3b9f4"
340 )
341 .expect("valid hash")
342 ))
343 ]
344 );
345 }
346
347 #[test]
348 fn from_str_rejects_wrong_hrp() {
349 let other_hrp = bech32::Hrp::parse("abcd").expect("valid hrp");
350 let encoded = bech32::encode::<bech32::Bech32m>(other_hrp, &[0u8; 8]).expect("encodes");
351 assert!(matches!(
352 InviteCode::from_str(&encoded),
353 Err(InviteCodeParseError::InvalidHrp { hrp }) if hrp == other_hrp
354 ));
355 }
356
357 #[test]
358 fn from_str_reports_the_whole_decode_chain_for_a_truncated_payload() {
359 use crate::encoding::Encodable;
360 use crate::util::FmtCompact as _;
361
362 let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
366 let invite = InviteCode::from_str(invite_code_str).expect("valid invite code");
367 let bytes = invite.consensus_encode_to_vec();
368 let encoded = bech32::encode::<bech32::Bech32m>(BECH32_HRP, &bytes[..bytes.len() - 1])
369 .expect("encodes");
370
371 let err = InviteCode::from_str(&encoded).expect_err("payload is truncated");
372 let text = err.to_string();
373 let InviteCodeParseError::Decode(inner) = err else {
374 panic!("a truncated payload is a decode error: {err:?}");
375 };
376
377 assert_eq!(
378 text,
379 format!("Invalid invite code payload: {}", inner.fmt_compact())
380 );
381 assert_ne!(
382 inner.fmt_compact().to_string(),
383 inner.to_string(),
384 "the decode error has more than one layer, and the message shows all of them"
385 );
386 }
387
388 #[test]
389 fn from_str_reports_the_whole_decode_chain_for_a_truncated_prefixed_payload() {
390 use crate::base32::PrefixedDecodeError;
391 use crate::encoding::Encodable;
392 use crate::util::FmtCompact as _;
393
394 let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
397 let invite = InviteCode::from_str(invite_code_str).expect("valid invite code");
398 let bytes = invite.consensus_encode_to_vec();
399 let encoded =
400 crate::base32::encode_prefixed_bytes(FEDIMINT_PREFIX, &bytes[..bytes.len() - 1]);
401
402 let err = InviteCode::from_str(&encoded).expect_err("payload is truncated");
403 let text = err.to_string();
404 let err_fmt_compact = err.fmt_compact().to_string();
405 let InviteCodeParseError::Base32(PrefixedDecodeError::ConsensusDecode(inner)) = err else {
406 panic!("a truncated payload is a decode error: {err:?}");
407 };
408
409 assert_eq!(
410 text,
411 format!(
412 "Invalid prefixed base32 invite code: \
413 Invalid consensus encoding in base32 payload: {}",
414 inner.fmt_compact()
415 )
416 );
417 assert_eq!(
418 err_fmt_compact, text,
419 "no source, so nothing is printed twice"
420 );
421 assert_ne!(
422 inner.fmt_compact().to_string(),
423 inner.to_string(),
424 "the decode error has more than one layer, and the message shows all of them"
425 );
426 }
427
428 #[test]
429 fn from_str_rejects_non_bech32() {
430 assert!(matches!(
431 InviteCode::from_str("definitely not bech32"),
432 Err(InviteCodeParseError::Bech32(_))
433 ));
434 }
435
436 #[test]
437 fn from_str_reports_corrupt_prefixed_base32() {
438 use crate::util::FmtCompact as _;
439
440 let err = InviteCode::from_str(&format!("{FEDIMINT_PREFIX}not!base32"))
441 .expect_err("not!base32 is not valid base32");
442 assert!(matches!(err, InviteCodeParseError::Base32(_)), "{err:?}");
443 assert_eq!(err.fmt_compact().to_string(), err.to_string());
444 }
445}