Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit 461cabb

Browse files
Merge pull request #1193 from MutinyWallet/dedup-fedimint-ann
Better dedup logic for nostr mints
2 parents 2418d55 + dab8267 commit 461cabb

1 file changed

Lines changed: 121 additions & 60 deletions

File tree

mutiny-core/src/nostr/mod.rs

Lines changed: 121 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,59 @@ impl NostrDiscoveredFedimint {
272272
}
273273
}
274274
}
275+
276+
pub fn merge(&mut self, other: &Self) {
277+
// merge metadata
278+
if let Some(other) = other.metadata.as_ref() {
279+
match self.metadata.as_mut() {
280+
Some(self_metadata) => {
281+
if self_metadata.name.is_none() {
282+
self_metadata.name = other.name.clone();
283+
}
284+
if self_metadata.display_name.is_none() {
285+
self_metadata.display_name = other.display_name.clone();
286+
}
287+
if self_metadata.picture.is_none() {
288+
self_metadata.picture = other.picture.clone();
289+
}
290+
}
291+
None => {
292+
self.metadata = Some(other.clone());
293+
}
294+
}
295+
}
296+
// merge created_at
297+
match self.created_at.as_ref() {
298+
Some(self_created_at) => {
299+
if other
300+
.created_at
301+
.is_some_and(|other| *self_created_at > other)
302+
{
303+
self.created_at = other.created_at;
304+
}
305+
}
306+
None => {
307+
self.created_at = other.created_at;
308+
}
309+
}
310+
311+
// merge invite codes
312+
let mut invite_codes = self.invite_codes.clone();
313+
for other_invite_code in other.invite_codes.iter() {
314+
if !invite_codes.contains(other_invite_code) {
315+
invite_codes.push(other_invite_code.clone());
316+
}
317+
}
318+
319+
self.invite_codes = invite_codes;
320+
321+
// merge expire_timestamp
322+
if let Some(other) = other.expire_timestamp {
323+
if other < self.expire_timestamp.unwrap_or(0) {
324+
self.expire_timestamp = Some(other);
325+
}
326+
}
327+
}
275328
}
276329

277330
impl<S: MutinyStorage, P: PrimalApi, C: NostrClient> NostrManager<S, P, C> {
@@ -2045,70 +2098,77 @@ impl<S: MutinyStorage, P: PrimalApi, C: NostrClient> NostrManager<S, P, C> {
20452098
)
20462099
.await?;
20472100

2048-
let mut mints: Vec<NostrDiscoveredFedimint> = events
2049-
.iter()
2050-
.filter_map(|event| {
2051-
// only process federation announcements
2052-
if event.kind != Kind::from(38173) {
2053-
return None;
2054-
}
2101+
let mut mints: HashMap<FederationId, NostrDiscoveredFedimint> = HashMap::new();
20552102

2056-
let network_tag = event.tags.iter().find_map(|tag| {
2057-
if tag.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::N))
2058-
{
2059-
Some(tag.as_vec().get(1).cloned().unwrap_or_default())
2060-
} else {
2061-
None
2062-
}
2063-
});
2103+
for event in events.iter() {
2104+
// only process federation announcements
2105+
if event.kind != Kind::from(38173) {
2106+
continue;
2107+
}
20642108

2065-
// if the network tag is missing, we assume it is on mainnet
2066-
let network_tag = network_tag
2067-
.as_deref()
2068-
.unwrap_or(network_to_string(Network::Bitcoin));
2069-
// skip if the network doesn't match
2070-
if network_tag != network_str {
2071-
return None;
2109+
let network_tag = event.tags.iter().find_map(|tag| {
2110+
if tag.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::N)) {
2111+
Some(tag.as_vec().get(1).cloned().unwrap_or_default())
2112+
} else {
2113+
None
20722114
}
2115+
});
20732116

2074-
let federation_id = event.tags.iter().find_map(|tag| {
2075-
if let Tag::Identifier(id) = tag {
2076-
FederationId::from_str(id).ok()
2077-
} else {
2078-
None
2079-
}
2080-
})?;
2081-
2082-
let invite_codes: Vec<InviteCode> = event
2083-
.tags
2084-
.iter()
2085-
.filter_map(|tag| parse_invite_code_from_tag(tag, &federation_id))
2086-
.collect();
2117+
// if the network tag is missing, we assume it is on mainnet
2118+
let network_tag = network_tag
2119+
.as_deref()
2120+
.unwrap_or(network_to_string(Network::Bitcoin));
2121+
// skip if the network doesn't match
2122+
if network_tag != network_str {
2123+
continue;
2124+
}
20872125

2088-
// if we have no invite codes left, skip
2089-
if invite_codes.is_empty() {
2090-
None
2126+
let federation_id = event.tags.iter().find_map(|tag| {
2127+
if let Tag::Identifier(id) = tag {
2128+
FederationId::from_str(id).ok()
20912129
} else {
2092-
// try to parse the metadata if available, it's okay if it fails
2093-
// todo could lookup kind 0 of the federation to get the metadata as well
2094-
let metadata = serde_json::from_str(&event.content).ok();
2095-
Some(NostrDiscoveredFedimint {
2096-
invite_codes,
2097-
id: federation_id,
2098-
pubkey: Some(event.pubkey),
2099-
event_id: Some(event.id),
2100-
created_at: Some(event.created_at.as_u64()),
2101-
metadata,
2102-
recommendations: vec![], // we'll add these in the next step
2103-
expire_timestamp: None,
2104-
})
2130+
None
21052131
}
2106-
})
2107-
.collect();
2132+
});
2133+
2134+
let federation_id = match federation_id {
2135+
Some(id) => id,
2136+
None => continue,
2137+
};
2138+
2139+
let invite_codes: Vec<InviteCode> = event
2140+
.tags
2141+
.iter()
2142+
.filter_map(|tag| parse_invite_code_from_tag(tag, &federation_id))
2143+
.collect();
21082144

2109-
// remove duplicates by federation id, keep the one with the newest event
2110-
mints.sort_by(|a, b| b.created_at.cmp(&a.created_at));
2111-
mints.dedup_by(|a, b| a.id == b.id);
2145+
// if we have no invite codes left, skip
2146+
if !invite_codes.is_empty() {
2147+
// try to parse the metadata if available, it's okay if it fails
2148+
// todo could lookup kind 0 of the federation to get the metadata as well
2149+
let metadata = serde_json::from_str(&event.content).ok();
2150+
let mint = NostrDiscoveredFedimint {
2151+
invite_codes,
2152+
id: federation_id,
2153+
pubkey: Some(event.pubkey),
2154+
event_id: Some(event.id),
2155+
created_at: Some(event.created_at.as_u64()),
2156+
metadata,
2157+
recommendations: vec![], // we'll add these in the next step
2158+
expire_timestamp: None,
2159+
};
2160+
2161+
match mints.get_mut(&federation_id) {
2162+
Some(m) => {
2163+
// if we already have a mint for this federation, merge the two
2164+
m.merge(&mint);
2165+
}
2166+
None => {
2167+
mints.insert(federation_id, mint);
2168+
}
2169+
}
2170+
}
2171+
}
21122172

21132173
// add on contact recommendations to mints
21142174
for event in events {
@@ -2170,8 +2230,8 @@ impl<S: MutinyStorage, P: PrimalApi, C: NostrClient> NostrManager<S, P, C> {
21702230

21712231
// todo read `a` tag recommendations as well
21722232

2173-
match mints.iter_mut().find(|m| m.id == federation_id) {
2174-
Some(mint) => {
2233+
match mints.iter_mut().find(|(_, m)| m.id == federation_id) {
2234+
Some((_id, mint)) => {
21752235
mint.recommendations.push(contact);
21762236
}
21772237
None => {
@@ -2189,14 +2249,14 @@ impl<S: MutinyStorage, P: PrimalApi, C: NostrClient> NostrManager<S, P, C> {
21892249
recommendations: vec![contact],
21902250
expire_timestamp: None,
21912251
};
2192-
mints.push(mint);
2252+
mints.insert(federation_id, mint);
21932253
}
21942254
}
21952255
}
21962256
}
21972257

21982258
// sort the recommendations by whether they are contacts or not and if they have an image
2199-
for mint in mints.iter_mut() {
2259+
for (_, mint) in mints.iter_mut() {
22002260
mint.recommendations.sort_by(|a, b| {
22012261
let a_is_contact = a
22022262
.npub
@@ -2228,6 +2288,7 @@ impl<S: MutinyStorage, P: PrimalApi, C: NostrClient> NostrManager<S, P, C> {
22282288
}
22292289

22302290
// sort mints by most recommended then by oldest
2291+
let mut mints: Vec<NostrDiscoveredFedimint> = mints.into_values().collect();
22312292
mints.sort();
22322293

22332294
// try to get federation info from client config if not in event

0 commit comments

Comments
 (0)