Relay-strategy hardening (network-trust §3): don't hang on one relay. - NostrChannel interface: what the transports need (sign, publish, subscribe, reqOnce). Implemented by NostrConnection (single relay) and the new RelayPool. - RelayPool: connects to all configured relays (skips unreachable ones, throws only when none are reachable), publishes to every live relay (accepted if any accepts), merges discovery deduped by event id, and survives a relay dropping. - Transports now depend on NostrChannel, not a concrete connection. - SocialService.openSession takes ALL configured relays (was relays.first); createOffersCubit passes the whole list. commons_core 63 tests green (3 new pool tests over two in-process relays); app_seeds social/market 18 green.
74 lines
2.6 KiB
Dart
74 lines
2.6 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:commons_core/commons_core.dart';
|
|
|
|
/// The app-side entry point to the (Block 2) social layer.
|
|
///
|
|
/// Holds the user's Nostr identity — deterministically derived from the same
|
|
/// root seed the recovery QR backs up (so it needs no extra backup) — and opens
|
|
/// social sessions on demand. Local-first: constructing this is cheap and
|
|
/// offline; nothing connects to a relay until [openSession] is called, so the
|
|
/// app runs fully without network and the social layer only enriches.
|
|
class SocialService {
|
|
SocialService({required this.identity, List<String> relays = const []})
|
|
: relays = List.unmodifiable(relays);
|
|
|
|
/// The derived Nostr identity (secp256k1). Same key across reinstalls that
|
|
/// restore the same seed.
|
|
final NostrIdentity identity;
|
|
|
|
/// Community relay URLs (may be empty — discovery/publishing degrade to
|
|
/// nothing when offline or unconfigured).
|
|
final List<String> relays;
|
|
|
|
/// Shareable public identity (`npub…`) and its hex form.
|
|
String get npub => identity.npub;
|
|
String get publicKeyHex => identity.publicKeyHex;
|
|
|
|
/// Derives the identity from the stored root-seed hex (32-byte, 64 chars).
|
|
static Future<SocialService> fromRootSeedHex(
|
|
String rootSeedHex, {
|
|
List<String> relays = const [],
|
|
}) async {
|
|
final identity = await NostrKeyDerivation.deriveFromSeed(
|
|
_hexToBytes(rootSeedHex),
|
|
);
|
|
return SocialService(identity: identity, relays: relays);
|
|
}
|
|
|
|
/// Opens a session against [relayUrls]: one fault-tolerant pool carrying all
|
|
/// three transports (offers, messaging, trust). Relays that are down are
|
|
/// skipped; throws only when none are reachable. Caller closes it.
|
|
Future<SocialSession> openSession(List<String> relayUrls) async {
|
|
final pool = await RelayPool.connect(relayUrls, identity: identity);
|
|
return SocialSession(pool);
|
|
}
|
|
}
|
|
|
|
/// One relay channel with the three transports on top — the
|
|
/// "one channel, three interfaces" shape, at the app layer.
|
|
class SocialSession {
|
|
SocialSession(this._channel)
|
|
: offers = NostrOfferTransport(_channel),
|
|
messages = NostrMessageTransport(_channel),
|
|
trust = NostrTrustTransport(_channel);
|
|
|
|
final NostrChannel _channel;
|
|
|
|
final OfferTransport offers;
|
|
final MessageTransport messages;
|
|
final TrustTransport trust;
|
|
|
|
Future<void> close() => _channel.close();
|
|
}
|
|
|
|
Uint8List _hexToBytes(String hex) {
|
|
if (hex.length.isOdd) {
|
|
throw ArgumentError('hex string must have an even length');
|
|
}
|
|
final out = Uint8List(hex.length ~/ 2);
|
|
for (var i = 0; i < out.length; i++) {
|
|
out[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
|
}
|
|
return out;
|
|
}
|