diff --git a/packages/ndk/lib/config/nip85_defaults.dart b/packages/ndk/lib/config/nip85_defaults.dart new file mode 100644 index 000000000..1f9f61cd4 --- /dev/null +++ b/packages/ndk/lib/config/nip85_defaults.dart @@ -0,0 +1,189 @@ +// ignore_for_file: constant_identifier_names + +import '../domain_layer/entities/nip_85.dart'; + +/// Default relay for NIP-85 trusted assertions +const String DEFAULT_NIP85_RELAY = 'wss://nip85.uid.ovh'; + +/// Default trusted providers for NIP-85 assertions +const List DEFAULT_NIP85_PROVIDERS = [ + // =========================================================================== + // USER METRICS (kind 30382) + // =========================================================================== + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.followers, + pubkey: '3116ea6afb590a19455d7b39ae3317c62b2bbb236986788b7e0ae12ec2281101', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.rank, + pubkey: 'cbeb151f3f5c4925c392c2b79936c3acd3c5c73a8ad60173ee6e43ff9112cfe1', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.firstCreatedAt, + pubkey: '367afc8744a62f99f0d6aa70b7a5506f57c302aff3ea72cf7cc7346371f82a25', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.postCount, + pubkey: 'c3e5ec4ccddc5b8535dab6dd3db3281923ee280f9aea6295ed68b571fc296fbc', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.replyCount, + pubkey: '9527682db2b48a7430fcd08750e2f925fc7b8cc2a2cb324239eb136ff06fe712', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.reactionsCount, + pubkey: '971c2269983ff452d945e32168486ff258003d5b778acbfe889bda54fc06a0b5', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAmountReceived, + pubkey: '91a82c20fc3f020f990aae0c26b6661a91926cb702d9bd9f8f2a2a1cbf72bc22', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAmountSent, + pubkey: '86175feae9b938c8a399680c1be162cd47a9aac00e78ab0069b3f7831793ceb2', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapCountReceived, + pubkey: 'f2066eb230b2fd7937c1e93d98b308de4c1fefa041c17604bd944164cbf8701d', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapCountSent, + pubkey: '780c6f1e2b531c710bcaf87c25ec9d3e927a2de1a72c3e22ee68077d30430c98', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAvgAmountDayReceived, + pubkey: 'a30c5ffee52284e21a7acca7aab8d45ed1f1f5461d802a602e887c7a1f680955', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAvgAmountDaySent, + pubkey: '3ee65e119f9d0feaaaa18a001ccfe186bd050e2f4c1e679ae0fbeedfd2f53ca3', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.reportsCountReceived, + pubkey: 'e9229572ed302c073ee1e30834f3d964a809532966c2082302574cfa1638931a', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.reportsCountSent, + pubkey: 'c7b8bdc6212193586a1a5c5053f0221705a8b9060650b942f0069adbbf77b346', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.activeHoursStart, + pubkey: 'e9087d8dbdfe211eb9b6e112aa78d313300a2ac48fa5ef0cbc480fd96c1bd558', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.activeHoursEnd, + pubkey: '816548ad83107b9adae3074df90a0faa85004559cda47b8d7da2bafebcd79b05', + relay: DEFAULT_NIP85_RELAY, + ), + + // =========================================================================== + // EVENT METRICS (kind 30383) + // =========================================================================== + Nip85TrustedProvider( + kind: Nip85Kind.event, + metric: Nip85Metric.commentCount, + pubkey: '6352c486e590896220b5964d46225bc57059a90971273ddfa50ed2da6cae8eaa', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.event, + metric: Nip85Metric.quoteCount, + pubkey: '35ad1c3d023180e1fd96dece9a51bfeed721dafb0a80e72517f338aae1b1f7de', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.event, + metric: Nip85Metric.repostCount, + pubkey: 'b18ceb02a32e73994685ddd2f417d042d88c676e0da715e802b994701c78f61b', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.event, + metric: Nip85Metric.reactionCount, + pubkey: '52251a59c4a62b0bd7dc3b3786b7ebf51f9cb6d2907123fc82334e4903612926', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.event, + metric: Nip85Metric.zapCount, + pubkey: 'ef072743789afc5cbf93d7862d3be744a4d34562606444f9405062a3cd774971', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.event, + metric: Nip85Metric.zapAmount, + pubkey: '2ee673c52e97c5a92815f9d4555365196860121a17a1cc54c06818f5d21c59bf', + relay: DEFAULT_NIP85_RELAY, + ), + + // =========================================================================== + // ADDRESSABLE METRICS (kind 30384) + // =========================================================================== + Nip85TrustedProvider( + kind: Nip85Kind.addressable, + metric: Nip85Metric.commentCount, + pubkey: '6352c486e590896220b5964d46225bc57059a90971273ddfa50ed2da6cae8eaa', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.addressable, + metric: Nip85Metric.quoteCount, + pubkey: '35ad1c3d023180e1fd96dece9a51bfeed721dafb0a80e72517f338aae1b1f7de', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.addressable, + metric: Nip85Metric.repostCount, + pubkey: 'b18ceb02a32e73994685ddd2f417d042d88c676e0da715e802b994701c78f61b', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.addressable, + metric: Nip85Metric.reactionCount, + pubkey: '52251a59c4a62b0bd7dc3b3786b7ebf51f9cb6d2907123fc82334e4903612926', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.addressable, + metric: Nip85Metric.zapCount, + pubkey: 'ef072743789afc5cbf93d7862d3be744a4d34562606444f9405062a3cd774971', + relay: DEFAULT_NIP85_RELAY, + ), + Nip85TrustedProvider( + kind: Nip85Kind.addressable, + metric: Nip85Metric.zapAmount, + pubkey: '2ee673c52e97c5a92815f9d4555365196860121a17a1cc54c06818f5d21c59bf', + relay: DEFAULT_NIP85_RELAY, + ), +]; diff --git a/packages/ndk/lib/domain_layer/entities/nip_85.dart b/packages/ndk/lib/domain_layer/entities/nip_85.dart new file mode 100644 index 000000000..d70fd346f --- /dev/null +++ b/packages/ndk/lib/domain_layer/entities/nip_85.dart @@ -0,0 +1,318 @@ +/// NIP-85 Event kinds for trusted assertions +class Nip85Kind { + /// User assertions (pubkey as subject) + static const int user = 30382; + + /// Event assertions (event_id as subject) + static const int event = 30383; + + /// Addressable event assertions (event_address as subject) + static const int addressable = 30384; + + /// External identifier assertions (NIP-73 i-tag as subject) + static const int externalId = 30385; + + static const List all = [user, event, addressable, externalId]; +} + +/// Available metrics for NIP-85 trusted assertions +enum Nip85Metric { + // User metrics (kind 30382) + followers('followers'), + firstCreatedAt('first_created_at'), + firstSeenAt('first_seen_at'), + postCount('post_cnt'), + replyCount('reply_cnt'), + reactionsCount('reactions_cnt'), + zapAmountReceived('zap_amt_recd'), + zapAmountSent('zap_amt_sent'), + zapCountReceived('zap_cnt_recd'), + zapCountSent('zap_cnt_sent'), + zapAvgAmountDayReceived('zap_avg_amt_day_recd'), + zapAvgAmountDaySent('zap_avg_amt_day_sent'), + reportsCountReceived('reports_cnt_recd'), + reportsCountSent('reports_cnt_sent'), + activeHoursStart('active_hours_start'), + activeHoursEnd('active_hours_end'), + + // Event/Addressable metrics (kinds 30383, 30384, 30385) + commentCount('comment_cnt'), + quoteCount('quote_cnt'), + repostCount('repost_cnt'), + reactionCount('reaction_cnt'), + zapCount('zap_cnt'), + zapAmount('zap_amount'), + + // Shared (all kinds) + rank('rank'); + + const Nip85Metric(this.tagName); + + /// The tag name used in NIP-85 events + final String tagName; + + /// Get metric from tag name + static Nip85Metric? fromTagName(String tagName) { + for (final metric in values) { + if (metric.tagName == tagName) return metric; + } + return null; + } +} + +/// Configuration for a NIP-85 trusted assertion provider +/// +/// Maps directly to NIP-85 kind 10040 tag format: +/// `[":", "", ""]` +class Nip85TrustedProvider { + /// The assertion kind this provider handles + final int kind; + + /// The specific metric this provider offers + final Nip85Metric metric; + + /// Provider's public key + final String pubkey; + + /// Relay URL where assertions are published + final String relay; + + const Nip85TrustedProvider({ + required this.kind, + required this.metric, + required this.pubkey, + required this.relay, + }); + + /// Create from NIP-85 kind 10040 tag format + /// `["30382:rank", "pubkey", "relay"]` + static Nip85TrustedProvider? fromTag(List tag) { + if (tag.length < 3) return null; + + final kindMetric = tag[0].split(':'); + if (kindMetric.length != 2) return null; + + final kind = int.tryParse(kindMetric[0]); + if (kind == null) return null; + + final metric = Nip85Metric.fromTagName(kindMetric[1]); + if (metric == null) return null; + + return Nip85TrustedProvider( + kind: kind, + metric: metric, + pubkey: tag[1], + relay: tag[2], + ); + } + + /// Convert to NIP-85 kind 10040 tag format + List toTag() => ['$kind:${metric.tagName}', pubkey, relay]; +} + +/// User metrics result from a NIP-85 assertion (kind 30382) +class Nip85UserMetrics { + /// Event kind for user assertions + static const int kKind = Nip85Kind.user; + + /// Subject pubkey (the user being asserted about) + final String pubkey; + + /// Provider pubkey (who made the assertion) + final String providerPubkey; + + /// Timestamp of the assertion + final int createdAt; + + /// Map of metric values + final Map metrics; + + /// Common topics (t tags) + final List topics; + + Nip85UserMetrics({ + required this.pubkey, + required this.providerPubkey, + required this.createdAt, + required this.metrics, + this.topics = const [], + }); + + /// Get a specific metric value + T? getMetric(Nip85Metric metric) { + final value = metrics[metric]; + if (value is T) return value; + return null; + } + + /// Get rank (0-100) + int? get rank => getMetric(Nip85Metric.rank); + + /// Get follower count + int? get followers => getMetric(Nip85Metric.followers); + + /// Get first created at timestamp + int? get firstCreatedAt => getMetric(Nip85Metric.firstCreatedAt); + + /// Get first seen at timestamp + int? get firstSeenAt => getMetric(Nip85Metric.firstSeenAt); + + /// Get post count + int? get postCount => getMetric(Nip85Metric.postCount); + + /// Get reply count + int? get replyCount => getMetric(Nip85Metric.replyCount); + + /// Get reactions count + int? get reactionsCount => getMetric(Nip85Metric.reactionsCount); + + /// Get zap amount received (sats) + int? get zapAmountReceived => getMetric(Nip85Metric.zapAmountReceived); + + /// Get zap amount sent (sats) + int? get zapAmountSent => getMetric(Nip85Metric.zapAmountSent); + + /// Get zap count received + int? get zapCountReceived => getMetric(Nip85Metric.zapCountReceived); + + /// Get zap count sent + int? get zapCountSent => getMetric(Nip85Metric.zapCountSent); +} + +/// Event metrics result from a NIP-85 assertion (kind 30383) +class Nip85EventMetrics { + /// Subject event ID + final String eventId; + + /// Provider pubkey (who made the assertion) + final String providerPubkey; + + /// Timestamp of the assertion + final int createdAt; + + /// Map of metric values + final Map metrics; + + Nip85EventMetrics({ + required this.eventId, + required this.providerPubkey, + required this.createdAt, + required this.metrics, + }); + + /// Get a specific metric value + T? getMetric(Nip85Metric metric) { + final value = metrics[metric]; + if (value is T) return value; + return null; + } + + /// Get rank (0-100) + int? get rank => getMetric(Nip85Metric.rank); + + /// Get comment count + int? get commentCount => getMetric(Nip85Metric.commentCount); + + /// Get quote count + int? get quoteCount => getMetric(Nip85Metric.quoteCount); + + /// Get repost count + int? get repostCount => getMetric(Nip85Metric.repostCount); + + /// Get reaction count + int? get reactionCount => getMetric(Nip85Metric.reactionCount); + + /// Get zap count + int? get zapCount => getMetric(Nip85Metric.zapCount); + + /// Get zap amount + int? get zapAmount => getMetric(Nip85Metric.zapAmount); +} + +/// Addressable event metrics result from a NIP-85 assertion (kind 30384) +class Nip85AddressableMetrics { + /// Subject event address (kind:pubkey:d-tag) + final String eventAddress; + + /// Provider pubkey (who made the assertion) + final String providerPubkey; + + /// Timestamp of the assertion + final int createdAt; + + /// Map of metric values + final Map metrics; + + Nip85AddressableMetrics({ + required this.eventAddress, + required this.providerPubkey, + required this.createdAt, + required this.metrics, + }); + + /// Get a specific metric value + T? getMetric(Nip85Metric metric) { + final value = metrics[metric]; + if (value is T) return value; + return null; + } + + /// Get rank (0-100) + int? get rank => getMetric(Nip85Metric.rank); + + /// Get comment count + int? get commentCount => getMetric(Nip85Metric.commentCount); + + /// Get quote count + int? get quoteCount => getMetric(Nip85Metric.quoteCount); + + /// Get repost count + int? get repostCount => getMetric(Nip85Metric.repostCount); + + /// Get reaction count + int? get reactionCount => getMetric(Nip85Metric.reactionCount); + + /// Get zap count + int? get zapCount => getMetric(Nip85Metric.zapCount); + + /// Get zap amount + int? get zapAmount => getMetric(Nip85Metric.zapAmount); +} + +/// External identifier metrics result from a NIP-85 assertion (kind 30385) +class Nip85ExternalIdMetrics { + /// Subject identifier (NIP-73 i-tag value) + final String identifier; + + /// Provider pubkey (who made the assertion) + final String providerPubkey; + + /// Timestamp of the assertion + final int createdAt; + + /// Map of metric values + final Map metrics; + + Nip85ExternalIdMetrics({ + required this.identifier, + required this.providerPubkey, + required this.createdAt, + required this.metrics, + }); + + /// Get a specific metric value + T? getMetric(Nip85Metric metric) { + final value = metrics[metric]; + if (value is T) return value; + return null; + } + + /// Get rank (0-100) + int? get rank => getMetric(Nip85Metric.rank); + + /// Get comment count + int? get commentCount => getMetric(Nip85Metric.commentCount); + + /// Get reaction count + int? get reactionCount => getMetric(Nip85Metric.reactionCount); +} diff --git a/packages/ndk/lib/domain_layer/usecases/ta/trusted_assertions.dart b/packages/ndk/lib/domain_layer/usecases/ta/trusted_assertions.dart new file mode 100644 index 000000000..b1923989e --- /dev/null +++ b/packages/ndk/lib/domain_layer/usecases/ta/trusted_assertions.dart @@ -0,0 +1,733 @@ +import 'dart:async'; + +import '../../entities/filter.dart'; +import '../../entities/nip_01_event.dart'; +import '../../entities/nip_85.dart'; +import '../requests/requests.dart'; + +/// Trusted Assertions usecase (NIP-85) +/// +/// Allows fetching pre-computed metrics from trusted service providers. +class TrustedAssertions { + final Requests _requests; + final List _defaultProviders; + + TrustedAssertions({ + required Requests requests, + required List defaultProviders, + }) : _requests = requests, + _defaultProviders = defaultProviders; + + /// Filter providers by kind and optionally by metrics + List _filterProviders( + List providers, { + required int kind, + Set? metrics, + }) { + return providers.where((p) { + // Must match the kind + if (p.kind != kind) return false; + // If metrics specified, must match one of them + if (metrics != null && metrics.isNotEmpty) { + return metrics.contains(p.metric); + } + return true; + }).toList(); + } + + /// Get user metrics from trusted providers + /// + /// [pubkey] - The public key of the user to get metrics for + /// [metrics] - Optional set of specific metrics to fetch. If null, fetches all available. + /// [providers] - Optional list of providers to use. If null, uses default providers. + /// + /// Returns [Nip85UserMetrics] or null if no assertion found. + Future getUserMetrics( + String pubkey, { + Set? metrics, + List? providers, + }) async { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.user, + metrics: metrics, + ); + + if (effectiveProviders.isEmpty) { + return null; + } + + // Group providers by relay for efficient querying + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + Nip85UserMetrics? result; + int latestCreatedAt = 0; + + // Query each relay + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + try { + await for (final event in _requests + .query( + filter: Filter( + kinds: [Nip85Kind.user], + authors: providerPubkeys, + dTags: [pubkey], + limit: providerPubkeys.length, + ), + explicitRelays: [relay], + cacheRead: true, + cacheWrite: true, + ) + .stream) { + final parsed = _parseUserMetricsEvent(event, metrics); + if (parsed != null && parsed.createdAt > latestCreatedAt) { + result = parsed; + latestCreatedAt = parsed.createdAt; + } + } + } catch (_) { + // Continue with other relays if one fails + } + } + + return result; + } + + /// Stream user metrics from trusted providers + /// + /// [pubkey] - The public key of the user to get metrics for + /// [metrics] - Optional set of specific metrics to fetch. If null, fetches all available. + /// [providers] - Optional list of providers to use. If null, uses default providers. + /// + /// Returns a [Stream] of [Nip85UserMetrics] that emits updates as they arrive. + Stream streamUserMetrics( + String pubkey, { + Set? metrics, + List? providers, + }) { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.user, + metrics: metrics, + ); + + final controller = StreamController(); + + if (effectiveProviders.isEmpty) { + controller.close(); + return controller.stream; + } + + // Group providers by relay + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + // Track subscriptions for cleanup + final subscriptionIds = []; + + // Subscribe to each relay + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + final response = _requests.subscription( + filter: Filter( + kinds: [Nip85Kind.user], + authors: providerPubkeys, + dTags: [pubkey], + ), + explicitRelays: [relay], + ); + + subscriptionIds.add(response.requestId); + + response.stream.listen( + (event) { + final parsed = _parseUserMetricsEvent(event, metrics); + if (parsed != null) { + controller.add(parsed); + } + }, + onError: (e) { + // Ignore errors from individual relays + }, + ); + } + + // Close subscriptions when stream is cancelled + controller.onCancel = () async { + for (final id in subscriptionIds) { + await _requests.closeSubscription(id); + } + }; + + return controller.stream; + } + + /// Parse a NIP-85 kind 30382 event into [Nip85UserMetrics] + Nip85UserMetrics? _parseUserMetricsEvent( + Nip01Event event, + Set? requestedMetrics, + ) { + if (event.kind != Nip85Kind.user) return null; + + final dTag = event.getDtag(); + if (dTag == null) return null; + + final metricsMap = {}; + final topics = []; + + for (final tag in event.tags) { + if (tag.length < 2) continue; + + final tagName = tag[0]; + final tagValue = tag[1]; + + // Handle topic tags + if (tagName == 't') { + topics.add(tagValue); + continue; + } + + // Try to parse as metric + final metric = Nip85Metric.fromTagName(tagName); + if (metric != null) { + // If specific metrics requested, filter + if (requestedMetrics != null && !requestedMetrics.contains(metric)) { + continue; + } + + // Parse value based on metric type + final parsedValue = int.tryParse(tagValue); + if (parsedValue != null) { + metricsMap[metric] = parsedValue; + } + } + } + + return Nip85UserMetrics( + pubkey: dTag, + providerPubkey: event.pubKey, + createdAt: event.createdAt, + metrics: metricsMap, + topics: topics, + ); + } + + // =========================================================================== + // EVENT METRICS (kind 30383) + // =========================================================================== + + /// Get event metrics from trusted providers + /// + /// [eventId] - The event ID to get metrics for + /// [metrics] - Optional set of specific metrics to fetch. If null, fetches all available. + /// [providers] - Optional list of providers to use. If null, uses default providers. + /// + /// Returns [Nip85EventMetrics] or null if no assertion found. + Future getEventMetrics( + String eventId, { + Set? metrics, + List? providers, + }) async { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.event, + metrics: metrics, + ); + + if (effectiveProviders.isEmpty) { + return null; + } + + // Group providers by relay for efficient querying + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + Nip85EventMetrics? result; + int latestCreatedAt = 0; + + // Query each relay + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + try { + await for (final event in _requests + .query( + filter: Filter( + kinds: [Nip85Kind.event], + authors: providerPubkeys, + dTags: [eventId], + limit: providerPubkeys.length, + ), + explicitRelays: [relay], + cacheRead: true, + cacheWrite: true, + ) + .stream) { + final parsed = _parseEventMetricsEvent(event, metrics); + if (parsed != null && parsed.createdAt > latestCreatedAt) { + result = parsed; + latestCreatedAt = parsed.createdAt; + } + } + } catch (_) { + // Continue with other relays if one fails + } + } + + return result; + } + + /// Stream event metrics from trusted providers + /// + /// [eventId] - The event ID to get metrics for + /// [metrics] - Optional set of specific metrics to fetch. If null, fetches all available. + /// [providers] - Optional list of providers to use. If null, uses default providers. + /// + /// Returns a [Stream] of [Nip85EventMetrics] that emits updates as they arrive. + Stream streamEventMetrics( + String eventId, { + Set? metrics, + List? providers, + }) { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.event, + metrics: metrics, + ); + + final controller = StreamController(); + + if (effectiveProviders.isEmpty) { + controller.close(); + return controller.stream; + } + + // Group providers by relay + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + // Track subscriptions for cleanup + final subscriptionIds = []; + + // Subscribe to each relay + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + final response = _requests.subscription( + filter: Filter( + kinds: [Nip85Kind.event], + authors: providerPubkeys, + dTags: [eventId], + ), + explicitRelays: [relay], + ); + + subscriptionIds.add(response.requestId); + + response.stream.listen( + (event) { + final parsed = _parseEventMetricsEvent(event, metrics); + if (parsed != null) { + controller.add(parsed); + } + }, + onError: (e) { + // Ignore errors from individual relays + }, + ); + } + + // Close subscriptions when stream is cancelled + controller.onCancel = () async { + for (final id in subscriptionIds) { + await _requests.closeSubscription(id); + } + }; + + return controller.stream; + } + + /// Parse a NIP-85 kind 30383 event into [Nip85EventMetrics] + Nip85EventMetrics? _parseEventMetricsEvent( + Nip01Event event, + Set? requestedMetrics, + ) { + if (event.kind != Nip85Kind.event) return null; + + final dTag = event.getDtag(); + if (dTag == null) return null; + + final metricsMap = {}; + + for (final tag in event.tags) { + if (tag.length < 2) continue; + + final tagName = tag[0]; + final tagValue = tag[1]; + + // Try to parse as metric + final metric = Nip85Metric.fromTagName(tagName); + if (metric != null) { + // If specific metrics requested, filter + if (requestedMetrics != null && !requestedMetrics.contains(metric)) { + continue; + } + + // Parse value based on metric type + final parsedValue = int.tryParse(tagValue); + if (parsedValue != null) { + metricsMap[metric] = parsedValue; + } + } + } + + return Nip85EventMetrics( + eventId: dTag, + providerPubkey: event.pubKey, + createdAt: event.createdAt, + metrics: metricsMap, + ); + } + + // =========================================================================== + // ADDRESSABLE METRICS (kind 30384) + // =========================================================================== + + /// Get addressable event metrics from trusted providers + /// + /// [eventAddress] - The event address (kind:pubkey:d-tag) to get metrics for + /// [metrics] - Optional set of specific metrics to fetch. If null, fetches all available. + /// [providers] - Optional list of providers to use. If null, uses default providers. + /// + /// Returns [Nip85AddressableMetrics] or null if no assertion found. + Future getAddressableMetrics( + String eventAddress, { + Set? metrics, + List? providers, + }) async { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.addressable, + metrics: metrics, + ); + + if (effectiveProviders.isEmpty) { + return null; + } + + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + Nip85AddressableMetrics? result; + int latestCreatedAt = 0; + + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + try { + await for (final event in _requests + .query( + filter: Filter( + kinds: [Nip85Kind.addressable], + authors: providerPubkeys, + dTags: [eventAddress], + limit: providerPubkeys.length, + ), + explicitRelays: [relay], + cacheRead: true, + cacheWrite: true, + ) + .stream) { + final parsed = _parseAddressableMetricsEvent(event, metrics); + if (parsed != null && parsed.createdAt > latestCreatedAt) { + result = parsed; + latestCreatedAt = parsed.createdAt; + } + } + } catch (_) {} + } + + return result; + } + + /// Stream addressable event metrics from trusted providers + Stream streamAddressableMetrics( + String eventAddress, { + Set? metrics, + List? providers, + }) { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.addressable, + metrics: metrics, + ); + + final controller = StreamController(); + + if (effectiveProviders.isEmpty) { + controller.close(); + return controller.stream; + } + + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + final subscriptionIds = []; + + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + final response = _requests.subscription( + filter: Filter( + kinds: [Nip85Kind.addressable], + authors: providerPubkeys, + dTags: [eventAddress], + ), + explicitRelays: [relay], + ); + + subscriptionIds.add(response.requestId); + + response.stream.listen( + (event) { + final parsed = _parseAddressableMetricsEvent(event, metrics); + if (parsed != null) { + controller.add(parsed); + } + }, + onError: (e) {}, + ); + } + + controller.onCancel = () async { + for (final id in subscriptionIds) { + await _requests.closeSubscription(id); + } + }; + + return controller.stream; + } + + Nip85AddressableMetrics? _parseAddressableMetricsEvent( + Nip01Event event, + Set? requestedMetrics, + ) { + if (event.kind != Nip85Kind.addressable) return null; + + final dTag = event.getDtag(); + if (dTag == null) return null; + + final metricsMap = {}; + + for (final tag in event.tags) { + if (tag.length < 2) continue; + + final metric = Nip85Metric.fromTagName(tag[0]); + if (metric != null) { + if (requestedMetrics != null && !requestedMetrics.contains(metric)) { + continue; + } + final parsedValue = int.tryParse(tag[1]); + if (parsedValue != null) { + metricsMap[metric] = parsedValue; + } + } + } + + return Nip85AddressableMetrics( + eventAddress: dTag, + providerPubkey: event.pubKey, + createdAt: event.createdAt, + metrics: metricsMap, + ); + } + + // =========================================================================== + // EXTERNAL ID METRICS (kind 30385) + // =========================================================================== + + /// Get external identifier metrics from trusted providers (NIP-73) + /// + /// [identifier] - The NIP-73 i-tag value to get metrics for + /// [metrics] - Optional set of specific metrics to fetch. If null, fetches all available. + /// [providers] - Optional list of providers to use. If null, uses default providers. + /// + /// Returns [Nip85ExternalIdMetrics] or null if no assertion found. + Future getExternalIdMetrics( + String identifier, { + Set? metrics, + List? providers, + }) async { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.externalId, + metrics: metrics, + ); + + if (effectiveProviders.isEmpty) { + return null; + } + + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + Nip85ExternalIdMetrics? result; + int latestCreatedAt = 0; + + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + try { + await for (final event in _requests + .query( + filter: Filter( + kinds: [Nip85Kind.externalId], + authors: providerPubkeys, + dTags: [identifier], + limit: providerPubkeys.length, + ), + explicitRelays: [relay], + cacheRead: true, + cacheWrite: true, + ) + .stream) { + final parsed = _parseExternalIdMetricsEvent(event, metrics); + if (parsed != null && parsed.createdAt > latestCreatedAt) { + result = parsed; + latestCreatedAt = parsed.createdAt; + } + } + } catch (_) {} + } + + return result; + } + + /// Stream external identifier metrics from trusted providers (NIP-73) + Stream streamExternalIdMetrics( + String identifier, { + Set? metrics, + List? providers, + }) { + final effectiveProviders = _filterProviders( + providers ?? _defaultProviders, + kind: Nip85Kind.externalId, + metrics: metrics, + ); + + final controller = StreamController(); + + if (effectiveProviders.isEmpty) { + controller.close(); + return controller.stream; + } + + final providersByRelay = >{}; + for (final provider in effectiveProviders) { + providersByRelay.putIfAbsent(provider.relay, () => []).add(provider); + } + + final subscriptionIds = []; + + for (final entry in providersByRelay.entries) { + final relay = entry.key; + final relayProviders = entry.value; + final providerPubkeys = relayProviders.map((p) => p.pubkey).toList(); + + final response = _requests.subscription( + filter: Filter( + kinds: [Nip85Kind.externalId], + authors: providerPubkeys, + dTags: [identifier], + ), + explicitRelays: [relay], + ); + + subscriptionIds.add(response.requestId); + + response.stream.listen( + (event) { + final parsed = _parseExternalIdMetricsEvent(event, metrics); + if (parsed != null) { + controller.add(parsed); + } + }, + onError: (e) {}, + ); + } + + controller.onCancel = () async { + for (final id in subscriptionIds) { + await _requests.closeSubscription(id); + } + }; + + return controller.stream; + } + + Nip85ExternalIdMetrics? _parseExternalIdMetricsEvent( + Nip01Event event, + Set? requestedMetrics, + ) { + if (event.kind != Nip85Kind.externalId) return null; + + final dTag = event.getDtag(); + if (dTag == null) return null; + + final metricsMap = {}; + + for (final tag in event.tags) { + if (tag.length < 2) continue; + + final metric = Nip85Metric.fromTagName(tag[0]); + if (metric != null) { + if (requestedMetrics != null && !requestedMetrics.contains(metric)) { + continue; + } + final parsedValue = int.tryParse(tag[1]); + if (parsedValue != null) { + metricsMap[metric] = parsedValue; + } + } + } + + return Nip85ExternalIdMetrics( + identifier: dTag, + providerPubkey: event.pubKey, + createdAt: event.createdAt, + metrics: metricsMap, + ); + } +} diff --git a/packages/ndk/lib/ndk.dart b/packages/ndk/lib/ndk.dart index cd572d8bb..6b59e1ed0 100644 --- a/packages/ndk/lib/ndk.dart +++ b/packages/ndk/lib/ndk.dart @@ -86,6 +86,9 @@ export 'domain_layer/usecases/fetched_ranges/fetched_ranges.dart'; export 'domain_layer/entities/filter_fetched_ranges.dart'; export 'domain_layer/usecases/proof_of_work/proof_of_work.dart'; export 'domain_layer/entities/nip_01_utils.dart'; +export 'domain_layer/usecases/ta/trusted_assertions.dart'; +export 'domain_layer/entities/nip_85.dart'; +export 'config/nip85_defaults.dart'; /** * other stuff diff --git a/packages/ndk/lib/presentation_layer/init.dart b/packages/ndk/lib/presentation_layer/init.dart index c31b18da7..8c0fec9a4 100644 --- a/packages/ndk/lib/presentation_layer/init.dart +++ b/packages/ndk/lib/presentation_layer/init.dart @@ -36,6 +36,7 @@ import '../domain_layer/usecases/relay_sets/relay_sets.dart'; import '../domain_layer/usecases/relay_sets_engine.dart'; import '../domain_layer/usecases/requests/requests.dart'; import '../domain_layer/usecases/search/search.dart'; +import '../domain_layer/usecases/ta/trusted_assertions.dart'; import '../domain_layer/usecases/user_relay_lists/user_relay_lists.dart'; import '../domain_layer/usecases/zaps/zaps.dart'; import '../shared/logger/logger.dart'; @@ -83,6 +84,7 @@ class Initialization { late Connectivy connectivity; late FetchedRanges fetchedRanges; late ProofOfWork proofOfWork; + late TrustedAssertions trustedAssertions; late Nip05Usecase nip05; @@ -263,6 +265,11 @@ class Initialization { proofOfWork = ProofOfWork(); + trustedAssertions = TrustedAssertions( + requests: requests, + defaultProviders: _ndkConfig.defaultTrustedProviders, + ); + /// set the user configured log level Logger.setLogLevel(_ndkConfig.logLevel); } diff --git a/packages/ndk/lib/presentation_layer/ndk.dart b/packages/ndk/lib/presentation_layer/ndk.dart index 6b0e32dbe..14d407397 100644 --- a/packages/ndk/lib/presentation_layer/ndk.dart +++ b/packages/ndk/lib/presentation_layer/ndk.dart @@ -22,6 +22,7 @@ import '../domain_layer/usecases/relay_manager.dart'; import '../domain_layer/usecases/relay_sets/relay_sets.dart'; import '../domain_layer/usecases/requests/requests.dart'; import '../domain_layer/usecases/search/search.dart'; +import '../domain_layer/usecases/ta/trusted_assertions.dart'; import '../domain_layer/usecases/user_relay_lists/user_relay_lists.dart'; import '../domain_layer/usecases/zaps/zaps.dart'; import 'init.dart'; @@ -156,6 +157,12 @@ class Ndk { @experimental FetchedRanges get fetchedRanges => _initialization.fetchedRanges; + /// Trusted Assertions (NIP-85) + /// + /// Fetch pre-computed metrics from trusted service providers. + @experimental + TrustedAssertions get ta => _initialization.trustedAssertions; + /// Close all transports on relay manager Future destroy() async { final allFutures = [ diff --git a/packages/ndk/lib/presentation_layer/ndk_config.dart b/packages/ndk/lib/presentation_layer/ndk_config.dart index a5a1d608a..3a86c5cff 100644 --- a/packages/ndk/lib/presentation_layer/ndk_config.dart +++ b/packages/ndk/lib/presentation_layer/ndk_config.dart @@ -1,9 +1,11 @@ import 'package:ndk/config/broadcast_defaults.dart'; import '../config/bootstrap_relays.dart'; +import '../config/nip85_defaults.dart'; import '../config/logger_defaults.dart'; import '../config/request_defaults.dart'; import '../domain_layer/entities/event_filter.dart'; +import '../domain_layer/entities/nip_85.dart'; import '../domain_layer/repositories/cache_manager.dart'; import '../domain_layer/repositories/event_verifier.dart'; import '../shared/logger/log_level.dart'; @@ -70,6 +72,9 @@ class NdkConfig { /// Defaults to 30 seconds. Duration authCallbackTimeout; + /// Default trusted providers for NIP-85 trusted assertions. + List defaultTrustedProviders; + /// Creates a new instance of [NdkConfig]. /// /// [eventVerifier] The verifier used to validate Nostr events. \ @@ -97,6 +102,7 @@ class NdkConfig { this.fetchedRangesEnabled = false, this.eagerAuth = false, this.authCallbackTimeout = RequestDefaults.DEFAULT_AUTH_CALLBACK_TIMEOUT, + this.defaultTrustedProviders = DEFAULT_NIP85_PROVIDERS, }); } diff --git a/packages/ndk/test/mocks/mock_relay.dart b/packages/ndk/test/mocks/mock_relay.dart index 334eff4dc..6fafa9eb9 100644 --- a/packages/ndk/test/mocks/mock_relay.dart +++ b/packages/ndk/test/mocks/mock_relay.dart @@ -22,6 +22,7 @@ class MockRelay { Map? textNotes; Map _contactLists = {}; Map _metadatas = {}; + Map _nip85Assertions = {}; // NIP-85 assertions keyed by "author:dTag" final Set _storedEvents = {}; // Store received events final Map> _activeSubscriptions = {}; // Track active subscriptions @@ -71,6 +72,7 @@ class MockRelay { Map? textNotes, Map? contactLists, Map? metadatas, + Map? nip85Assertions, Duration? delayResponse, }) async { var myPromise = Completer(); @@ -87,6 +89,9 @@ class MockRelay { if (metadatas != null) { _metadatas = metadatas; } + if (nip85Assertions != null) { + _nip85Assertions = nip85Assertions; + } var server = await HttpServer.bind(InternetAddress.loopbackIPv4, _port!, shared: true); @@ -276,6 +281,20 @@ class MockRelay { .where((e) => filter.authors!.contains(e.pubKey)) .toList()); } + // Match against NIP-85 assertions (kinds 30382-30385) + else if (filter.kinds != null && + filter.kinds!.any((k) => k >= 30382 && k <= 30385) && + filter.authors != null && + filter.authors!.isNotEmpty) { + eventsForThisFilter.addAll(_nip85Assertions.values.where((e) { + bool kindMatches = filter.kinds!.contains(e.kind); + bool authorMatches = filter.authors!.contains(e.pubKey); + bool dTagMatches = filter.dTags == null || + filter.dTags!.isEmpty || + filter.dTags!.contains(e.getDtag()); + return kindMatches && authorMatches && dTagMatches; + }).toList()); + } // General event matching (storedEvents and textNotes) else { eventsForThisFilter.addAll(_storedEvents.where((event) { diff --git a/packages/ndk/test/usecases/ta/trusted_assertions_test.dart b/packages/ndk/test/usecases/ta/trusted_assertions_test.dart new file mode 100644 index 000000000..bb15870ac --- /dev/null +++ b/packages/ndk/test/usecases/ta/trusted_assertions_test.dart @@ -0,0 +1,228 @@ +import 'package:ndk/ndk.dart'; +import 'package:ndk/shared/nips/nip01/bip340.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_event_verifier.dart'; +import '../../mocks/mock_relay.dart'; + +void main() { + group('TrustedAssertions', () { + late MockRelay relay; + late Ndk ndk; + + // Provider keys + final providerKey = Bip340.generatePrivateKey(); + final providerSigner = Bip340EventSigner( + privateKey: providerKey.privateKey, + publicKey: providerKey.publicKey, + ); + + // Subject (user being rated) + final subjectKey = Bip340.generatePrivateKey(); + + // Create a NIP-85 user assertion event (kind 30382) + Nip01Event createNip85UserAssertion({ + required String providerPubkey, + required String subjectPubkey, + int rank = 85, + int followers = 1000, + int postCount = 500, + }) { + return Nip01Event( + pubKey: providerPubkey, + kind: Nip85Kind.user, + tags: [ + ['d', subjectPubkey], + ['rank', rank.toString()], + ['followers', followers.toString()], + ['post_cnt', postCount.toString()], + ['t', 'nostr'], + ['t', 'bitcoin'], + ], + content: '', + ); + } + + setUp(() async { + relay = MockRelay(name: 'nip85-relay', explicitPort: 5196); + + // Create assertion event + final assertionEvent = await providerSigner.sign( + createNip85UserAssertion( + providerPubkey: providerKey.publicKey, + subjectPubkey: subjectKey.publicKey, + rank: 89, + followers: 2500, + postCount: 150, + ), + ); + + // Start relay with NIP-85 assertions + await relay.startServer( + nip85Assertions: { + '${providerKey.publicKey}:${subjectKey.publicKey}': assertionEvent, + }, + ); + + // Configure NDK with trusted provider + final config = NdkConfig( + eventVerifier: MockEventVerifier(), + cache: MemCacheManager(), + engine: NdkEngine.RELAY_SETS, + bootstrapRelays: [relay.url], + defaultTrustedProviders: [ + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.rank, + pubkey: providerKey.publicKey, + relay: relay.url, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.followers, + pubkey: providerKey.publicKey, + relay: relay.url, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.postCount, + pubkey: providerKey.publicKey, + relay: relay.url, + ), + ], + ); + + ndk = Ndk(config); + await ndk.relays.seedRelaysConnected; + }); + + tearDown(() async { + await ndk.destroy(); + await relay.stopServer(); + }); + + test('getUserMetrics returns metrics from provider', () async { + final metrics = await ndk.ta.getUserMetrics(subjectKey.publicKey); + + expect(metrics, isNotNull); + expect(metrics!.pubkey, equals(subjectKey.publicKey)); + expect(metrics.providerPubkey, equals(providerKey.publicKey)); + expect(metrics.rank, equals(89)); + expect(metrics.followers, equals(2500)); + expect(metrics.postCount, equals(150)); + expect(metrics.topics, contains('nostr')); + expect(metrics.topics, contains('bitcoin')); + }); + + test('getUserMetrics returns null for unknown pubkey', () async { + final unknownKey = Bip340.generatePrivateKey(); + final metrics = await ndk.ta.getUserMetrics(unknownKey.publicKey); + + expect(metrics, isNull); + }); + + test('getUserMetrics filters specific metrics', () async { + final metrics = await ndk.ta.getUserMetrics( + subjectKey.publicKey, + metrics: {Nip85Metric.rank, Nip85Metric.followers}, + ); + + expect(metrics, isNotNull); + expect(metrics!.rank, equals(89)); + expect(metrics.followers, equals(2500)); + // postCount should not be included when filtering + expect(metrics.postCount, isNull); + }); + + test('getUserMetrics returns null when no providers configured', () async { + // Create NDK without trusted providers + final ndkNoProviders = Ndk(NdkConfig( + eventVerifier: MockEventVerifier(), + cache: MemCacheManager(), + engine: NdkEngine.RELAY_SETS, + bootstrapRelays: [relay.url], + defaultTrustedProviders: [], + )); + + await ndkNoProviders.relays.seedRelaysConnected; + + final metrics = await ndkNoProviders.ta.getUserMetrics( + subjectKey.publicKey, + ); + + expect(metrics, isNull); + + await ndkNoProviders.destroy(); + }); + + test('getUserMetrics with custom providers', () async { + // Create a second provider + final provider2Key = Bip340.generatePrivateKey(); + final provider2Signer = Bip340EventSigner( + privateKey: provider2Key.privateKey, + publicKey: provider2Key.publicKey, + ); + + final relay2 = MockRelay(name: 'nip85-relay-2', explicitPort: 5197); + + final assertionEvent2 = await provider2Signer.sign( + createNip85UserAssertion( + providerPubkey: provider2Key.publicKey, + subjectPubkey: subjectKey.publicKey, + rank: 75, + followers: 500, + postCount: 50, + ), + ); + + await relay2.startServer( + nip85Assertions: { + '${provider2Key.publicKey}:${subjectKey.publicKey}': assertionEvent2, + }, + ); + + // Use custom provider instead of default + final metrics = await ndk.ta.getUserMetrics( + subjectKey.publicKey, + providers: [ + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.rank, + pubkey: provider2Key.publicKey, + relay: relay2.url, + ), + ], + ); + + expect(metrics, isNotNull); + expect(metrics!.rank, equals(75)); + expect(metrics.providerPubkey, equals(provider2Key.publicKey)); + + await relay2.stopServer(); + }); + + test('Nip85TrustedProvider.fromTag parses correctly', () { + final tag = ['30382:rank', 'abc123', 'wss://relay.example.com']; + final provider = Nip85TrustedProvider.fromTag(tag); + + expect(provider, isNotNull); + expect(provider!.kind, equals(30382)); + expect(provider.metric, equals(Nip85Metric.rank)); + expect(provider.pubkey, equals('abc123')); + expect(provider.relay, equals('wss://relay.example.com')); + }); + + test('Nip85TrustedProvider.toTag converts correctly', () { + final provider = Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.followers, + pubkey: 'xyz789', + relay: 'wss://test.relay', + ); + + final tag = provider.toTag(); + + expect(tag, equals(['30382:followers', 'xyz789', 'wss://test.relay'])); + }); + }); +} diff --git a/packages/ndk/test/usecases/ta/trusted_assertions_test_2.dart b/packages/ndk/test/usecases/ta/trusted_assertions_test_2.dart new file mode 100644 index 000000000..7aa07447f --- /dev/null +++ b/packages/ndk/test/usecases/ta/trusted_assertions_test_2.dart @@ -0,0 +1,133 @@ +import 'package:ndk/ndk.dart'; + +final relay = "wss://nip85.uid.ovh"; +// final relay = "ws://localhost:3334"; + +final providers = [ + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.followers, + pubkey: "3116ea6afb590a19455d7b39ae3317c62b2bbb236986788b7e0ae12ec2281101", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.rank, + pubkey: "cbeb151f3f5c4925c392c2b79936c3acd3c5c73a8ad60173ee6e43ff9112cfe1", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.firstCreatedAt, + pubkey: "367afc8744a62f99f0d6aa70b7a5506f57c302aff3ea72cf7cc7346371f82a25", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.firstSeenAt, + pubkey: "2c04e8c33beeba6578f2a0b3ba290f8e368a8e267bfa2c5ab5b96a5f60d826ab", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.postCount, + pubkey: "c3e5ec4ccddc5b8535dab6dd3db3281923ee280f9aea6295ed68b571fc296fbc", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.replyCount, + pubkey: "9527682db2b48a7430fcd08750e2f925fc7b8cc2a2cb324239eb136ff06fe712", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.reactionsCount, + pubkey: "971c2269983ff452d945e32168486ff258003d5b778acbfe889bda54fc06a0b5", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAmountReceived, + pubkey: "91a82c20fc3f020f990aae0c26b6661a91926cb702d9bd9f8f2a2a1cbf72bc22", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAmountSent, + pubkey: "86175feae9b938c8a399680c1be162cd47a9aac00e78ab0069b3f7831793ceb2", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapCountReceived, + pubkey: "f2066eb230b2fd7937c1e93d98b308de4c1fefa041c17604bd944164cbf8701d", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapCountSent, + pubkey: "780c6f1e2b531c710bcaf87c25ec9d3e927a2de1a72c3e22ee68077d30430c98", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAvgAmountDayReceived, + pubkey: "a30c5ffee52284e21a7acca7aab8d45ed1f1f5461d802a602e887c7a1f680955", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.zapAvgAmountDaySent, + pubkey: "3ee65e119f9d0feaaaa18a001ccfe186bd050e2f4c1e679ae0fbeedfd2f53ca3", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.reportsCountReceived, + pubkey: "e9229572ed302c073ee1e30834f3d964a809532966c2082302574cfa1638931a", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.reportsCountSent, + pubkey: "c7b8bdc6212193586a1a5c5053f0221705a8b9060650b942f0069adbbf77b346", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.activeHoursStart, + pubkey: "e9087d8dbdfe211eb9b6e112aa78d313300a2ac48fa5ef0cbc480fd96c1bd558", + relay: relay, + ), + Nip85TrustedProvider( + kind: Nip85Kind.user, + metric: Nip85Metric.activeHoursEnd, + pubkey: "816548ad83107b9adae3074df90a0faa85004559cda47b8d7da2bafebcd79b05", + relay: relay, + ), +]; + +void main() { + final ndk = Ndk( + NdkConfig( + eventVerifier: Bip340EventVerifier(), + cache: MemCacheManager(), + // defaultTrustedProviders: providers, + ), + ); + + final npub = + "npub1s5yq6wadwrxde4lhfs56gn64hwzuhnfa6r9mj476r5s4hkunzgzqrs6q7z"; + + final pubkey = Nip19.decode(npub); + + print("Start"); + + final stream = + ndk.ta.streamUserMetrics(pubkey, metrics: {Nip85Metric.reactionsCount}); + + stream.listen((metrics) { + print('postCount: ${metrics.reactionsCount}'); + }); +}