import 'package:commons_core/commons_core.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Where a peer stands, from strongest to weakest signal. enum TrustTier { /// Passes the full Duniter membership rule (sigQty certifications from members, /// within stepMax of a bootstrap referent). networkMember, /// In your personal circle (you vouch, or a friend-of-a-friend does). inYourCircle, /// Vouched for by someone, but not (yet) a member nor in your circle. vouched, /// No certifications seen. unknown, } /// Trust standing of one peer: the full Duniter membership verdict, your own /// circle, and the raw certifier count — computed from the public certification /// graph with the active [WotParams] and bootstrap referents. class TrustState extends Equatable { const TrustState({ this.certifierCount = 0, this.iVouch = false, this.knownToYou = false, this.isNetworkMember = false, this.networkBootstrapped = false, this.loading = true, this.busy = false, }); /// How many people (network-wide) currently vouch for the peer. final int certifierCount; /// Whether this user vouches for the peer. final bool iVouch; /// Whether the peer is within the user's own circle (you, or a /// friend-of-a-friend, vouch). Spam-resistant and works from day one. final bool knownToYou; /// Whether the peer is a full member per the Duniter rule (sigQty + stepMax /// from the bootstrap referents). final bool isNetworkMember; /// Whether any bootstrap referents exist at all. When false, network /// membership can't be determined yet (the trust net isn't seeded) and the UI /// should say so instead of implying the peer failed the rule. final bool networkBootstrapped; final bool loading; final bool busy; /// The strongest applicable signal, for the badge. TrustTier get tier { if (isNetworkMember) return TrustTier.networkMember; if (knownToYou) return TrustTier.inYourCircle; if (certifierCount > 0) return TrustTier.vouched; return TrustTier.unknown; } TrustState copyWith({ int? certifierCount, bool? iVouch, bool? knownToYou, bool? isNetworkMember, bool? networkBootstrapped, bool? loading, bool? busy, }) => TrustState( certifierCount: certifierCount ?? this.certifierCount, iVouch: iVouch ?? this.iVouch, knownToYou: knownToYou ?? this.knownToYou, isNetworkMember: isNetworkMember ?? this.isNetworkMember, networkBootstrapped: networkBootstrapped ?? this.networkBootstrapped, loading: loading ?? this.loading, busy: busy ?? this.busy, ); @override List get props => [ certifierCount, iVouch, knownToYou, isNetworkMember, networkBootstrapped, loading, busy, ]; } /// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport], /// and computes the full Duniter membership verdict against the bootstrap /// [referents] and active [params]. class TrustCubit extends Cubit { TrustCubit( this._transport, { required this.peerPubkey, required this.selfPubkey, this.referents = const {}, this.params = WotParams.duniter, Future Function()? onDispose, }) : _onDispose = onDispose, super(const TrustState()); final TrustTransport? _transport; final String peerPubkey; final String selfPubkey; /// Bootstrap referents (Duniter seeds) the membership rule trusts by /// construction. Empty = the network isn't seeded yet. final Set referents; /// Active web-of-trust parameters (sigQty / stepMax / validity). final WotParams params; final Future Function()? _onDispose; bool get isOnline => _transport != null; /// Personal "circle" rule: one vouch from your side, out to a friend-of-a- /// friend. Loose by design — "people near you", separate from formal /// membership. static const _circleThreshold = 1; static const _circleDistance = 2; /// Loads the certification graph and computes: the peer's certifier count, /// whether you vouch, whether they're in your circle, and whether they pass /// the full Duniter membership rule against the bootstrap referents. Future load() async { final transport = _transport; if (transport == null) { emit(state.copyWith(loading: false)); return; } emit(state.copyWith(loading: true)); final wot = WebOfTrust.fromCertifications( await transport.allCertifications(), now: DateTime.now(), ); final certifiers = wot.certifiersOf(peerPubkey); final circle = wot.members( seeds: {selfPubkey}, threshold: _circleThreshold, maxDistance: _circleDistance, ); // Full membership: only meaningful once the trust net has referents. final members = referents.isEmpty ? const {} : wot.membersWith(seeds: referents, params: params); emit(state.copyWith( certifierCount: certifiers.length, iVouch: certifiers.contains(selfPubkey), knownToYou: circle.contains(peerPubkey), isNetworkMember: members.contains(peerPubkey), networkBootstrapped: referents.isNotEmpty, loading: false, )); } /// Adds or removes this user's vouch, then reloads. Never vouches for self. /// The certification is issued with the active validity so it expires and /// must be renewed (Duniter rule). Future toggleVouch() async { final transport = _transport; if (transport == null || peerPubkey == selfPubkey) return; emit(state.copyWith(busy: true)); if (state.iVouch) { await transport.revoke(subjectPubkey: peerPubkey); } else { await transport.certify( subjectPubkey: peerPubkey, validity: params.sigValidity, ); } await load(); emit(state.copyWith(busy: false)); } @override Future close() async { await _onDispose?.call(); return super.close(); } }