Skip to content
This repository was archived by the owner on Dec 23, 2024. It is now read-only.

Commit 5a00d03

Browse files
committed
feat: Add graceful degradation for relays; Closes #95 and #96
1 parent 7a6bf09 commit 5a00d03

File tree

3 files changed

+117
-9
lines changed

3 files changed

+117
-9
lines changed

lib/api/nostr-relays.dart

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,112 @@
11
import 'dart:convert';
22

3+
import 'package:flutter_logs/flutter_logs.dart';
34
import "package:http/http.dart" as http;
45
import 'package:locus/constants/apis.dart';
56
import 'package:locus/constants/values.dart';
67

7-
Future<Map<String, dynamic>> getNostrRelays() async {
8-
final response = await http.get(Uri.parse(NOSTR_LIST_URI)).timeout(HTTP_TIMEOUT);
8+
final RELAY_FETCHER_FUNCTIONS = [
9+
NostrWatchAPI.getPublicNostrRelays,
10+
NostrBandAPI.getTrendingProfiles,
11+
NostrWatchAPI.getAllNostrRelays,
12+
() => Future.value(FALLBACK_RELAYS),
13+
];
14+
15+
// Tries each of the fallback relays until one works
16+
Future<Map<String, List<String>>> getNostrRelays() async {
17+
FlutterLogs.logInfo(LOG_TAG, "Get Nostr Relays", "Fetching Nostr Relays");
18+
19+
for (final fetch in RELAY_FETCHER_FUNCTIONS) {
20+
FlutterLogs.logInfo(LOG_TAG, "Get Nostr Relays", "Trying method $fetch");
21+
22+
try {
23+
final relays = await fetch();
24+
25+
return {
26+
"relays": relays,
27+
};
28+
} catch (error) {
29+
FlutterLogs.logError(
30+
LOG_TAG,
31+
"Get Nostr Relays",
32+
"Failed to fetch relays: $error",
33+
);
34+
}
35+
}
36+
37+
FlutterLogs.logWarn(
38+
LOG_TAG,
39+
"Get Nostr Relays",
40+
"Failed to fetch relays. Falling back to default relays",
41+
);
942

1043
return {
11-
"relays": List<String>.from(jsonDecode(response.body)),
44+
"relays": FALLBACK_RELAYS,
1245
};
1346
}
47+
48+
class NostrWatchAPI {
49+
static Future<List<String>> getPublicNostrRelays() async {
50+
final response = await http
51+
.get(Uri.parse(NOSTR_PUBLIC_RELAYS_LIST_URI))
52+
.timeout(HTTP_TIMEOUT);
53+
54+
return List<String>.from(jsonDecode(response.body));
55+
}
56+
57+
static Future<List<String>> getAllNostrRelays() async {
58+
final response = await http
59+
.get(Uri.parse(NOSTR_ONLINE_RELAYS_LIST_URI))
60+
.timeout(HTTP_TIMEOUT);
61+
62+
return List<String>.from(jsonDecode(response.body));
63+
}
64+
}
65+
66+
class NostrBandAPI {
67+
static Future<List<String>> getTrendingProfiles() async {
68+
final response = await http
69+
.get(Uri.parse(NOSTR_TRENDING_PROFILES_URI))
70+
.timeout(HTTP_TIMEOUT);
71+
72+
return List<String>.from(
73+
List<dynamic>.from(jsonDecode(response.body)["profiles"])
74+
.map((e) => e["relays"])
75+
.expand((element) => element)
76+
.toSet());
77+
}
78+
}
79+
80+
// Top 30 most used free relays
81+
const FALLBACK_RELAYS = [
82+
"relay.damus.io",
83+
"eden.nostr.land",
84+
"nos.lol",
85+
"relay.snort.social",
86+
"relay.current.fyi",
87+
"brb.io",
88+
"nostr.orangepill.dev",
89+
"nostr-pub.wellorder.net",
90+
"nostr.bitcoiner.social",
91+
"nostr.wine",
92+
"nostr.oxtr.dev",
93+
"relay.nostr.bg",
94+
"nostr.mom",
95+
"nostr.fmt.wiz.biz",
96+
"relay.nostr.band",
97+
"nostr-pub.semisol.dev",
98+
"nostr.milou.lol",
99+
"nostr.onsats.org",
100+
"relay.nostr.info",
101+
"puravida.nostr.land",
102+
"offchain.pub",
103+
"relay.orangepill.dev",
104+
"no.str.cr",
105+
"nostr.zebedee.cloud",
106+
"atlas.nostr.land",
107+
"nostr-relay.wlvs.space",
108+
"relay.nostrati.com",
109+
"relay.nostr.com.au",
110+
"relay.inosta.cc",
111+
"nostr.rocks",
112+
];

lib/constants/apis.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
const NOSTR_LIST_URI = "https://api.nostr.watch/v1/public";
1+
const NOSTR_PUBLIC_RELAYS_LIST_URI = "https://api.nostr.watch/v1/public";
2+
const NOSTR_ONLINE_RELAYS_LIST_URI = "https://api.nostr.watch/v1/online";
3+
const NOSTR_TRENDING_PROFILES_URI =
4+
"https://api.nostr.band/v0/trending/profiles";

lib/widgets/RelaySelectSheet.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import 'package:locus/widgets/ModalSheet.dart';
1515
import '../api/nostr-relays.dart';
1616
import '../utils/cache.dart';
1717

18-
String removeProtocol(final String url) => url.toLowerCase().replaceAll(RegExp(r'^wss://'), '');
18+
String removeProtocol(final String url) =>
19+
url.toLowerCase().replaceAll(RegExp(r'^wss://'), '');
1920

20-
String addProtocol(final String url) => url.toLowerCase().startsWith('wss://') ? url : 'wss://$url';
21+
String addProtocol(final String url) =>
22+
url.toLowerCase().startsWith('wss://') ? url : 'wss://$url';
2123

2224
class RelayController extends ChangeNotifier {
2325
late final List<String> _relays;
@@ -103,7 +105,8 @@ class _RelaySelectSheetState extends State<RelaySelectSheet> {
103105
return;
104106
}
105107

106-
final normalizedSelectedRelays = widget.controller.relays.map(removeProtocol);
108+
final normalizedSelectedRelays =
109+
widget.controller.relays.map(removeProtocol);
107110

108111
if (normalizedSelectedRelays.contains(value)) {
109112
setState(() {
@@ -201,8 +204,11 @@ class _RelaySelectSheetState extends State<RelaySelectSheet> {
201204
},
202205
extractValue: (dynamic element) => element as String,
203206
builder: (_, List<dynamic> foundRelays) {
204-
final uncheckedFoundRelays = foundRelays.where((element) => !checkedRelaysSet.contains(element)).toList();
205-
final allRelays = List<String>.from([...widget.controller.relays, ...uncheckedFoundRelays]);
207+
final uncheckedFoundRelays = foundRelays
208+
.where((element) => !checkedRelaysSet.contains(element))
209+
.toList();
210+
final allRelays = List<String>.from(
211+
[...widget.controller.relays, ...uncheckedFoundRelays]);
206212

207213
final length = allRelays.length + (isValueNew ? 1 : 0);
208214

0 commit comments

Comments
 (0)