1use core::fmt;
2use std::borrow::Cow;
3use std::collections::BTreeMap;
4use std::fmt::{Display, Formatter};
5use std::io::{Cursor, Read};
6use std::str::FromStr;
7
8use anyhow::ensure;
9use bech32::{Bech32m, Hrp};
10use serde::{Deserialize, Serialize};
11
12use crate::config::FederationId;
13use crate::encoding::{Decodable, DecodeError, Encodable};
14use crate::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
15use crate::util::SafeUrl;
16use crate::{NumPeersExt, PeerId};
17
18#[derive(Clone, Debug, Eq, PartialEq, Encodable, Hash, Ord, PartialOrd)]
27pub struct InviteCode(Vec<InviteCodePart>);
28
29impl Decodable for InviteCode {
30 fn consensus_decode<R: Read>(
31 r: &mut R,
32 modules: &ModuleDecoderRegistry,
33 ) -> Result<Self, DecodeError> {
34 let inner: Vec<InviteCodePart> = Decodable::consensus_decode(r, modules)?;
35
36 if !inner
37 .iter()
38 .any(|data| matches!(data, InviteCodePart::Api { .. }))
39 {
40 return Err(DecodeError::from_str(
41 "No API was provided in the invite code",
42 ));
43 }
44
45 if !inner
46 .iter()
47 .any(|data| matches!(data, InviteCodePart::FederationId(_)))
48 {
49 return Err(DecodeError::from_str(
50 "No Federation ID provided in invite code",
51 ));
52 }
53
54 Ok(Self(inner))
55 }
56}
57
58impl InviteCode {
59 pub fn new(
60 url: SafeUrl,
61 peer: PeerId,
62 federation_id: FederationId,
63 api_secret: Option<String>,
64 ) -> Self {
65 let mut s = Self(vec![
66 InviteCodePart::Api { url, peer },
67 InviteCodePart::FederationId(federation_id),
68 ]);
69
70 if let Some(api_secret) = api_secret {
71 s.0.push(InviteCodePart::ApiSecret(api_secret));
72 }
73
74 s
75 }
76
77 pub fn from_map(
78 peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
79 federation_id: FederationId,
80 api_secret: Option<String>,
81 ) -> Self {
82 let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
83 let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
84 .iter()
85 .take(max_size)
86 .map(|(peer, url)| InviteCodePart::Api {
87 url: url.clone(),
88 peer: *peer,
89 })
90 .collect();
91
92 code_vec.push(InviteCodePart::FederationId(federation_id));
93
94 if let Some(api_secret) = api_secret {
95 code_vec.push(InviteCodePart::ApiSecret(api_secret));
96 }
97
98 Self(code_vec)
99 }
100
101 pub fn new_with_essential_num_guardians(
104 peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
105 federation_id: FederationId,
106 ) -> Self {
107 let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
108 let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
109 .iter()
110 .take(max_size)
111 .map(|(peer, url)| InviteCodePart::Api {
112 url: url.clone(),
113 peer: *peer,
114 })
115 .collect();
116 code_vec.push(InviteCodePart::FederationId(federation_id));
117
118 Self(code_vec)
119 }
120
121 pub fn url(&self) -> SafeUrl {
123 self.0
124 .iter()
125 .find_map(|data| match data {
126 InviteCodePart::Api { url, .. } => Some(url.clone()),
127 _ => None,
128 })
129 .expect("Ensured by constructor")
130 }
131
132 pub fn api_secret(&self) -> Option<String> {
134 self.0.iter().find_map(|data| match data {
135 InviteCodePart::ApiSecret(api_secret) => Some(api_secret.clone()),
136 _ => None,
137 })
138 }
139 pub fn peer(&self) -> PeerId {
142 self.0
143 .iter()
144 .find_map(|data| match data {
145 InviteCodePart::Api { peer, .. } => Some(*peer),
146 _ => None,
147 })
148 .expect("Ensured by constructor")
149 }
150
151 pub fn peers(&self) -> BTreeMap<PeerId, SafeUrl> {
153 self.0
154 .iter()
155 .filter_map(|entry| match entry {
156 InviteCodePart::Api { url, peer } => Some((*peer, url.clone())),
157 _ => None,
158 })
159 .collect()
160 }
161
162 pub fn federation_id(&self) -> FederationId {
165 self.0
166 .iter()
167 .find_map(|data| match data {
168 InviteCodePart::FederationId(federation_id) => Some(*federation_id),
169 _ => None,
170 })
171 .expect("Ensured by constructor")
172 }
173}
174
175#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable, Hash, Ord, PartialOrd)]
184enum InviteCodePart {
185 Api {
187 url: SafeUrl,
189 peer: PeerId,
191 },
192
193 FederationId(FederationId),
195
196 ApiSecret(String),
198
199 #[encodable_default]
201 Default { variant: u64, bytes: Vec<u8> },
202}
203
204const BECH32_HRP: Hrp = Hrp::parse_unchecked("fed1");
212
213impl FromStr for InviteCode {
214 type Err = anyhow::Error;
215
216 fn from_str(encoded: &str) -> Result<Self, Self::Err> {
217 if let Ok(invite_code_v2) = InviteCodeV2::decode_base64(encoded) {
218 return invite_code_v2.into_v1();
219 }
220
221 let (hrp, data) = bech32::decode(encoded)?;
222
223 ensure!(hrp == BECH32_HRP, "Invalid HRP in bech32 encoding");
224
225 let invite = Self::consensus_decode(&mut Cursor::new(data), &ModuleRegistry::default())?;
226
227 Ok(invite)
228 }
229}
230
231impl Display for InviteCode {
233 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
234 let mut data = vec![];
235
236 self.consensus_encode(&mut data)
237 .expect("Vec<u8> provides capacity");
238
239 let encode = bech32::encode::<Bech32m>(BECH32_HRP, &data).map_err(|_| fmt::Error)?;
240 formatter.write_str(&encode)
241 }
242}
243
244impl Serialize for InviteCode {
245 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
246 where
247 S: serde::Serializer,
248 {
249 String::serialize(&self.to_string(), serializer)
250 }
251}
252
253impl<'de> Deserialize<'de> for InviteCode {
254 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
255 where
256 D: serde::Deserializer<'de>,
257 {
258 let string = Cow::<str>::deserialize(deserializer)?;
259 Self::from_str(&string).map_err(serde::de::Error::custom)
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use std::collections::BTreeMap;
266 use std::str::FromStr;
267
268 use fedimint_core::util::SafeUrl;
269 use fedimint_core::PeerId;
270
271 use crate::config::FederationId;
272 use crate::invite_code::{InviteCode, InviteCodeV2};
273
274 #[test]
275 fn test_invite_code_to_from_string() {
276 let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
277 let invite_code = InviteCode::from_str(invite_code_str).expect("valid invite code");
278
279 assert_eq!(invite_code.to_string(), invite_code_str);
280 assert_eq!(
281 invite_code.0,
282 [
283 crate::invite_code::InviteCodePart::Api {
284 url: "wss://fedimintd.mplsfed.foo/".parse().expect("valid url"),
285 peer: PeerId::new(0),
286 },
287 crate::invite_code::InviteCodePart::FederationId(FederationId(
288 bitcoin::hashes::sha256::Hash::from_str(
289 "bea7ff4116f2b1d324c7b5d699cce4ac7408cee41db2c88027e21b76fff3b9f4"
290 )
291 .expect("valid hash")
292 ))
293 ]
294 );
295 }
296
297 #[test]
298 fn invite_code_v2_encode_base64_roundtrip() {
299 let invite_code = InviteCodeV2 {
300 id: FederationId::dummy(),
301 peers: BTreeMap::from_iter([(
302 PeerId::from(0),
303 SafeUrl::parse("https://mint.com").expect("Url is valid"),
304 )]),
305 api_secret: None,
306 };
307
308 let encoded = invite_code.encode_base64();
309 let decoded = InviteCodeV2::decode_base64(&encoded).expect("Failed to decode");
310
311 assert_eq!(invite_code, decoded);
312
313 InviteCode::from_str(&encoded).expect("Failed to decode to legacy");
314 }
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encodable, Decodable)]
318pub struct InviteCodeV2 {
319 pub id: FederationId,
320 pub peers: BTreeMap<PeerId, SafeUrl>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 #[serde(default)]
323 pub api_secret: Option<String>,
324}
325
326impl InviteCodeV2 {
327 pub fn into_v1(self) -> anyhow::Result<InviteCode> {
328 Ok(InviteCode::from_map(&self.peers, self.id, self.api_secret))
329 }
330
331 pub fn encode_base64(&self) -> String {
332 let json = &serde_json::to_string(self).expect("Encoding to JSON cannot fail");
333 let base_64 = base64_url::encode(json);
334
335 format!("fedimintA{base_64}")
336 }
337
338 pub fn decode_base64(s: &str) -> anyhow::Result<Self> {
339 ensure!(s.starts_with("fedimintA"), "Invalid Prefix");
340
341 let invite_code: Self = serde_json::from_slice(&base64_url::decode(&s[9..])?)?;
342
343 ensure!(!invite_code.peers.is_empty(), "Invite code has no peer");
344
345 Ok(invite_code)
346 }
347}