import 'dart:async'; import 'nip99.dart'; import 'nostr_connection.dart'; import 'offer.dart'; import 'offer_transport.dart'; /// Nostr NIP-99 backend for [OfferTransport], on a shared [NostrConnection] /// (Q2/Q3). Signs offers with the derived key (Q1) and publishes them as /// kind-30402 classified listings; discovery is a REQ filtered by kind + `#g`. class NostrOfferTransport implements OfferTransport { NostrOfferTransport(this._conn); final NostrConnection _conn; final Nip99Codec _codec = Nip99Codec(); /// Convenience: open a dedicated connection for this transport. static Future connect( String relayUrl, { required String privateKeyHex, required String pubkeyHex, }) async => NostrOfferTransport(await NostrConnection.connect( relayUrl, privateKeyHex: privateKeyHex, publicKeyHex: pubkeyHex, )); Map _filter(DiscoveryQuery q) => { 'kinds': [Nip99Codec.kindActive], '#g': [q.geohashPrefix], 'limit': q.limit, }; @override Future publish(Offer offer) async { final event = _codec.encode( offer, createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000, )..signWith(_conn.privateKeyHex); final r = await _conn.publish(event); return PublishResult( accepted: r.accepted, transportRef: event.id, message: r.message, ); } @override Stream discover(DiscoveryQuery query) => _conn.subscribe(_filter(query)).map(_codec.decode).where( (o) => query.types.isEmpty || query.types.contains(o.type), ); /// Collect matches up to EOSE (for tests/metrics). Future> discoverUntilEose(DiscoveryQuery query) async { final events = await _conn.reqOnce(_filter(query)); return events .map(_codec.decode) .where((o) => query.types.isEmpty || query.types.contains(o.type)) .toList(); } @override Future retract(String offerId) async { // A NIP-09 deletion of the addressable coordinate would go here; the OK-path // is identical to publish. Omitted from the spike surface. } @override Future close() => _conn.close(); }