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 anyhow::ensure;
9use bech32::{Bech32m, Hrp};
10use serde::{Deserialize, Serialize};
11
12use crate::base32::FEDIMINT_PREFIX;
13use crate::config::FederationId;
14use crate::encoding::{Decodable, DecodeError, Encodable};
15use crate::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
16use crate::util::SafeUrl;
17use crate::{NumPeersExt, PeerId};
18
19#[derive(Clone, Debug, Eq, PartialEq, Encodable, Hash, Ord, PartialOrd)]
28pub struct InviteCode(Vec<InviteCodePart>);
29
30impl Decodable for InviteCode {
31 fn consensus_decode_partial<R: Read>(
32 r: &mut R,
33 modules: &ModuleDecoderRegistry,
34 ) -> Result<Self, DecodeError> {
35 let inner: Vec<InviteCodePart> = Decodable::consensus_decode_partial(r, modules)?;
36
37 if !inner
38 .iter()
39 .any(|data| matches!(data, InviteCodePart::Api { .. }))
40 {
41 return Err(DecodeError::from_str(
42 "No API was provided in the invite code",
43 ));
44 }
45
46 if !inner
47 .iter()
48 .any(|data| matches!(data, InviteCodePart::FederationId(_)))
49 {
50 return Err(DecodeError::from_str(
51 "No Federation ID provided in invite code",
52 ));
53 }
54
55 Ok(Self(inner))
56 }
57}
58
59impl InviteCode {
60 pub fn new(
61 url: SafeUrl,
62 peer: PeerId,
63 federation_id: FederationId,
64 api_secret: Option<String>,
65 ) -> Self {
66 let mut s = Self(vec![
67 InviteCodePart::Api { url, peer },
68 InviteCodePart::FederationId(federation_id),
69 ]);
70
71 if let Some(api_secret) = api_secret {
72 s.0.push(InviteCodePart::ApiSecret(api_secret));
73 }
74
75 s
76 }
77
78 pub fn from_map(
79 peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
80 federation_id: FederationId,
81 api_secret: Option<String>,
82 ) -> Self {
83 let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
84 let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
85 .iter()
86 .take(max_size)
87 .map(|(peer, url)| InviteCodePart::Api {
88 url: url.clone(),
89 peer: *peer,
90 })
91 .collect();
92
93 code_vec.push(InviteCodePart::FederationId(federation_id));
94
95 if let Some(api_secret) = api_secret {
96 code_vec.push(InviteCodePart::ApiSecret(api_secret));
97 }
98
99 Self(code_vec)
100 }
101
102 pub fn new_with_essential_num_guardians(
105 peer_to_url_map: &BTreeMap<PeerId, SafeUrl>,
106 federation_id: FederationId,
107 ) -> Self {
108 let max_size = peer_to_url_map.to_num_peers().max_evil() + 1;
109 let mut code_vec: Vec<InviteCodePart> = peer_to_url_map
110 .iter()
111 .take(max_size)
112 .map(|(peer, url)| InviteCodePart::Api {
113 url: url.clone(),
114 peer: *peer,
115 })
116 .collect();
117 code_vec.push(InviteCodePart::FederationId(federation_id));
118
119 Self(code_vec)
120 }
121
122 pub fn url(&self) -> SafeUrl {
124 self.0
125 .iter()
126 .find_map(|data| match data {
127 InviteCodePart::Api { url, .. } => Some(url.clone()),
128 _ => None,
129 })
130 .expect("Ensured by constructor")
131 }
132
133 pub fn api_secret(&self) -> Option<String> {
135 self.0.iter().find_map(|data| match data {
136 InviteCodePart::ApiSecret(api_secret) => Some(api_secret.clone()),
137 _ => None,
138 })
139 }
140 pub fn peer(&self) -> PeerId {
143 self.0
144 .iter()
145 .find_map(|data| match data {
146 InviteCodePart::Api { peer, .. } => Some(*peer),
147 _ => None,
148 })
149 .expect("Ensured by constructor")
150 }
151
152 pub fn peers(&self) -> BTreeMap<PeerId, SafeUrl> {
154 self.0
155 .iter()
156 .filter_map(|entry| match entry {
157 InviteCodePart::Api { url, peer } => Some((*peer, url.clone())),
158 _ => None,
159 })
160 .collect()
161 }
162
163 pub fn federation_id(&self) -> FederationId {
166 self.0
167 .iter()
168 .find_map(|data| match data {
169 InviteCodePart::FederationId(federation_id) => Some(*federation_id),
170 _ => None,
171 })
172 .expect("Ensured by constructor")
173 }
174}
175
176#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable, Hash, Ord, PartialOrd)]
185enum InviteCodePart {
186 Api {
188 url: SafeUrl,
190 peer: PeerId,
192 },
193
194 FederationId(FederationId),
196
197 ApiSecret(String),
199
200 #[encodable_default]
202 Default { variant: u64, bytes: Vec<u8> },
203}
204
205const BECH32_HRP: Hrp = Hrp::parse_unchecked("fed1");
213
214impl FromStr for InviteCode {
215 type Err = anyhow::Error;
216
217 fn from_str(encoded: &str) -> Result<Self, Self::Err> {
218 if let Ok(invite_code) = crate::base32::decode_prefixed(FEDIMINT_PREFIX, encoded) {
219 return Ok(invite_code);
220 }
221
222 let (hrp, data) = bech32::decode(encoded)?;
223
224 ensure!(hrp == BECH32_HRP, "Invalid HRP in bech32 encoding");
225
226 let invite = Self::consensus_decode_whole(&data, &ModuleRegistry::default())?;
227
228 Ok(invite)
229 }
230}
231
232impl Display for InviteCode {
234 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
235 let data = self.consensus_encode_to_vec();
236 let encode = bech32::encode::<Bech32m>(BECH32_HRP, &data).map_err(|_| fmt::Error)?;
237 formatter.write_str(&encode)
238 }
239}
240
241impl Serialize for InviteCode {
242 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
243 where
244 S: serde::Serializer,
245 {
246 String::serialize(&self.to_string(), serializer)
247 }
248}
249
250impl<'de> Deserialize<'de> for InviteCode {
251 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252 where
253 D: serde::Deserializer<'de>,
254 {
255 let string = Cow::<str>::deserialize(deserializer)?;
256 Self::from_str(&string).map_err(serde::de::Error::custom)
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use std::str::FromStr;
263
264 use fedimint_core::PeerId;
265 use fedimint_core::base32::FEDIMINT_PREFIX;
266
267 use crate::config::FederationId;
268 use crate::invite_code::InviteCode;
269
270 #[test]
271 fn test_invite_code_to_from_string() {
272 let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
273 let invite_code = InviteCode::from_str(invite_code_str).expect("valid invite code");
274
275 InviteCode::from_str(&crate::base32::encode_prefixed(
276 FEDIMINT_PREFIX,
277 &invite_code,
278 ))
279 .expect("Failed to parse base 32 invite code");
280
281 assert_eq!(invite_code.to_string(), invite_code_str);
282 assert_eq!(
283 invite_code.0,
284 [
285 crate::invite_code::InviteCodePart::Api {
286 url: "wss://fedimintd.mplsfed.foo/".parse().expect("valid url"),
287 peer: PeerId::new(0),
288 },
289 crate::invite_code::InviteCodePart::FederationId(FederationId(
290 bitcoin::hashes::sha256::Hash::from_str(
291 "bea7ff4116f2b1d324c7b5d699cce4ac7408cee41db2c88027e21b76fff3b9f4"
292 )
293 .expect("valid hash")
294 ))
295 ]
296 );
297 }
298}