/ app / lib / services / dead_drop_service.dart
dead_drop_service.dart
  1  import 'dart:async';
  2  import 'dart:convert';
  3  import 'dart:typed_data';
  4  
  5  import '../src/rust/api.dart' as rust_api;
  6  
  7  // Re-export generated types for convenience
  8  export '../src/rust/api.dart'
  9      show
 10          IdentityInfo,
 11          ContactInfo,
 12          PendingContact,
 13          MessageInfo,
 14          HandshakeHandle,
 15          HandshakeResult,
 16          ExchangeHandle,
 17          ExchangeResultDto,
 18          ExchangeComplete,
 19          DocumentInfo,
 20          SearchResultInfo,
 21          CachedOnionAddress,
 22          GossipExchangeHandle,
 23          GossipExchangeComplete,
 24          GroupInfo,
 25          GroupMemberInfoDto;
 26  
 27  /// Main service wrapping Rust core functionality via FFI.
 28  ///
 29  /// This service provides a Dart-friendly interface to the Dead Drop
 30  /// cryptographic core implemented in Rust. All cryptographic operations
 31  /// happen in Rust for security.
 32  ///
 33  /// Usage:
 34  /// ```dart
 35  /// final service = DeadDropService.instance;
 36  /// await service.initialize(storagePath, passphrase);
 37  /// ```
 38  class DeadDropService {
 39    static DeadDropService? _instance;
 40    bool _initialized = false;
 41  
 42    /// Stream controller for message queued events
 43    final _messageQueuedController = StreamController<Uint8List>.broadcast();
 44  
 45    /// Stream that emits contact IDs when a new message is queued
 46    Stream<Uint8List> get onMessageQueued => _messageQueuedController.stream;
 47  
 48    DeadDropService._();
 49  
 50    /// Singleton instance
 51    static DeadDropService get instance {
 52      _instance ??= DeadDropService._();
 53      return _instance!;
 54    }
 55  
 56    /// Whether the service has been initialized
 57    bool get isInitialized => _initialized;
 58  
 59    // ===========================================================================
 60    // INITIALIZATION
 61    // ===========================================================================
 62  
 63    /// Initialize the service with storage path and passphrase.
 64    ///
 65    /// Must be called before any other operations.
 66    /// The passphrase is used to encrypt the local database.
 67    Future<void> initialize(String storagePath, String passphrase) async {
 68      if (_initialized) return;
 69  
 70      rust_api.initialize(storagePath: storagePath, passphrase: passphrase);
 71      _initialized = true;
 72    }
 73  
 74    // ===========================================================================
 75    // IDENTITY
 76    // ===========================================================================
 77  
 78    /// Check if an identity exists in storage.
 79    Future<bool> hasIdentity() async {
 80      _ensureInitialized();
 81      return rust_api.hasIdentity();
 82    }
 83  
 84    /// Create a new identity (key pair generation).
 85    ///
 86    /// Returns the identity info including public key fingerprint.
 87    Future<rust_api.IdentityInfo> createIdentity() async {
 88      _ensureInitialized();
 89      return rust_api.createIdentity();
 90    }
 91  
 92    /// Get the current identity.
 93    Future<rust_api.IdentityInfo> getIdentity() async {
 94      _ensureInitialized();
 95      return rust_api.getIdentity();
 96    }
 97  
 98    // ===========================================================================
 99    // QR CODES
100    // ===========================================================================
101  
102    /// Generate QR payload for contact exchange.
103    ///
104    /// Returns a base64-encoded string suitable for QR code display.
105    Future<String> generateQrPayload() async {
106      _ensureInitialized();
107      return rust_api.generateQrPayload();
108    }
109  
110    /// Process a scanned QR code payload.
111    ///
112    /// Returns pending contact info if valid.
113    Future<rust_api.PendingContact> processQrCode(String payload) async {
114      _ensureInitialized();
115      return rust_api.processQrCode(payload: payload);
116    }
117  
118    /// Confirm and save a pending contact.
119    Future<void> confirmContact(
120      rust_api.PendingContact pending,
121      String? nickname,
122    ) async {
123      _ensureInitialized();
124      rust_api.confirmContact(pending: pending, nickname: nickname);
125    }
126  
127    // ===========================================================================
128    // CONTACTS
129    // ===========================================================================
130  
131    /// List all contacts.
132    Future<List<rust_api.ContactInfo>> listContacts() async {
133      _ensureInitialized();
134      return rust_api.listContacts();
135    }
136  
137    /// Get a specific contact by ID.
138    Future<rust_api.ContactInfo> getContact(Uint8List contactId) async {
139      _ensureInitialized();
140      return rust_api.getContact(contactId: contactId.toList());
141    }
142  
143    /// Delete a contact.
144    Future<void> deleteContact(Uint8List contactId) async {
145      _ensureInitialized();
146      rust_api.deleteContact(contactId: contactId.toList());
147    }
148  
149    /// Update a contact's nickname.
150    Future<void> updateContactNickname(
151      Uint8List contactId,
152      String? nickname,
153    ) async {
154      _ensureInitialized();
155      rust_api.updateContactNickname(
156        contactId: contactId.toList(),
157        nickname: nickname,
158      );
159    }
160  
161    /// Block a contact.
162    Future<void> blockContact(Uint8List contactId) async {
163      _ensureInitialized();
164      rust_api.blockContact(contactId: contactId.toList());
165    }
166  
167    /// Unblock a contact.
168    Future<void> unblockContact(Uint8List contactId) async {
169      _ensureInitialized();
170      rust_api.unblockContact(contactId: contactId.toList());
171    }
172  
173    // ===========================================================================
174    // MESSAGES
175    // ===========================================================================
176  
177    /// Send a text message to a contact.
178    ///
179    /// Returns the message ID.
180    Future<Uint8List> sendTextMessage(
181      Uint8List recipientId,
182      String text, {
183      int ttlDays = 30,
184    }) async {
185      _ensureInitialized();
186      final messageId = await rust_api.sendMessage(
187        recipientId: recipientId.toList(),
188        content: utf8.encode(text),
189        contentType: ContentType.text,
190        ttlDays: ttlDays,
191      );
192      // Notify listeners that a new message is queued for this contact
193      _messageQueuedController.add(recipientId);
194      return Uint8List.fromList(messageId);
195    }
196  
197    /// Send a document/file to a contact.
198    ///
199    /// For small files (<= 30KB), sends as a single message.
200    /// For large files, automatically chunks the file.
201    /// Returns the document ID.
202    Future<Uint8List> sendDocument(
203      Uint8List recipientId,
204      Uint8List content,
205      String filename, {
206      int ttlDays = 30,
207    }) async {
208      _ensureInitialized();
209      final documentId = await rust_api.sendDocument(
210        recipientId: recipientId.toList(),
211        content: content.toList(),
212        filename: filename,
213        ttlDays: ttlDays,
214      );
215      // Notify listeners that a new message is queued for this contact
216      _messageQueuedController.add(recipientId);
217      return Uint8List.fromList(documentId);
218    }
219  
220    /// Reassemble a chunked document from received chunks.
221    ///
222    /// Returns document info including content if complete.
223    Future<rust_api.DocumentInfo> reassembleDocument(Uint8List documentId) async {
224      _ensureInitialized();
225      return rust_api.reassembleDocument(documentId: documentId.toList());
226    }
227  
228    /// Check if all chunks have been received for a document.
229    bool isDocumentComplete(Uint8List documentId) {
230      _ensureInitialized();
231      return rust_api.isDocumentComplete(documentId: documentId.toList());
232    }
233  
234    /// Get document info (filename and size) without reassembling.
235    /// Returns (filename, size) tuple or null if not found.
236    (String, int)? getDocumentInfo(Uint8List documentId) {
237      _ensureInitialized();
238      final info = rust_api.getDocumentInfo(documentId: documentId.toList());
239      if (info == null) return null;
240      // info.$2 is BigInt for u64, convert to int
241      return (info.$1, info.$2.toInt());
242    }
243  
244    /// Get send progress for a chunked document.
245    /// Returns (delivered, total) or null if not a chunked send.
246    (int, int)? getSendProgress(Uint8List documentId) {
247      _ensureInitialized();
248      return rust_api.getSendProgress(documentId: documentId.toList());
249    }
250  
251    /// Get receive progress for a chunked document.
252    /// Returns (received, total) or null if not found.
253    (int, int)? getReceiveProgress(Uint8List documentId) {
254      _ensureInitialized();
255      return rust_api.getReceiveProgress(documentId: documentId.toList());
256    }
257  
258    /// Search messages within a conversation.
259    List<rust_api.SearchResultInfo> searchConversation(
260      Uint8List contactId,
261      String query, {
262      int limit = 50,
263    }) {
264      _ensureInitialized();
265      return rust_api.searchConversation(
266        contactId: contactId.toList(),
267        query: query,
268        limit: limit,
269      );
270    }
271  
272    /// Search all messages across all conversations.
273    List<rust_api.SearchResultInfo> searchAllMessages(
274      String query, {
275      int limit = 50,
276    }) {
277      _ensureInitialized();
278      return rust_api.searchAllMessages(query: query, limit: limit);
279    }
280  
281    /// Get conversation with a contact.
282    Future<List<rust_api.MessageInfo>> getConversation(
283      Uint8List contactId,
284    ) async {
285      _ensureInitialized();
286      return rust_api.getConversation(contactId: contactId.toList());
287    }
288  
289    /// Get outbound message queue for a contact.
290    Future<List<rust_api.MessageInfo>> getOutboundQueue(
291      Uint8List contactId,
292    ) async {
293      _ensureInitialized();
294      return rust_api.getOutboundQueue(contactId: contactId.toList());
295    }
296  
297    /// Mark a message as read.
298    void markMessageRead(Uint8List messageId) {
299      _ensureInitialized();
300      rust_api.markRead(messageId: messageId.toList());
301    }
302  
303    /// Delete a message.
304    Future<void> deleteMessage(Uint8List messageId) async {
305      _ensureInitialized();
306      rust_api.deleteMessage(messageId: messageId.toList());
307    }
308  
309    /// Clear all messages for a contact.
310    Future<int> clearConversation(Uint8List contactId) async {
311      _ensureInitialized();
312      return rust_api.clearConversation(contactId: contactId.toList());
313    }
314  
315    /// Get total unread message count.
316    Future<int> getUnreadCount() async {
317      _ensureInitialized();
318      return rust_api.getUnreadCount();
319    }
320  
321    // ===========================================================================
322    // GROUPS
323    // ===========================================================================
324  
325    /// Create a new group with the given name and members.
326    /// Returns the group ID (16 bytes).
327    Future<Uint8List> createGroup(String name, List<Uint8List> memberIds) async {
328      _ensureInitialized();
329      final groupId = await rust_api.createGroup(
330        name: name,
331        memberIds: memberIds,
332      );
333      return Uint8List.fromList(groupId);
334    }
335  
336    /// List all groups.
337    List<rust_api.GroupInfo> listGroups() {
338      _ensureInitialized();
339      return rust_api.listGroups();
340    }
341  
342    /// Get members of a group.
343    List<rust_api.GroupMemberInfoDto> getGroupMembers(Uint8List groupId) {
344      _ensureInitialized();
345      return rust_api.getGroupMembers(groupId: groupId.toList());
346    }
347  
348    /// Send a message to all group members (fan-out).
349    /// Returns the message ID.
350    Future<Uint8List> sendGroupMessage(
351      Uint8List groupId,
352      List<int> content,
353      int contentType, {
354      String? filename,
355    }) async {
356      _ensureInitialized();
357      final messageId = await rust_api.sendGroupMessage(
358        groupId: groupId.toList(),
359        content: content,
360        contentType: contentType,
361        filename: filename,
362      );
363      return Uint8List.fromList(messageId);
364    }
365  
366    /// Send a document to a group, automatically chunking if larger than 30KB.
367    /// Returns the document ID.
368    Future<Uint8List> sendGroupDocument(
369      Uint8List groupId,
370      Uint8List content,
371      String filename,
372    ) async {
373      _ensureInitialized();
374      final documentId = await rust_api.sendGroupDocument(
375        groupId: groupId.toList(),
376        content: content.toList(),
377        filename: filename,
378      );
379      return Uint8List.fromList(documentId);
380    }
381  
382    /// Accept a group invite.
383    void acceptGroupInvite(Uint8List groupId) {
384      _ensureInitialized();
385      rust_api.acceptGroupInvite(groupId: groupId.toList());
386    }
387  
388    /// Decline a group invite (notify members, then delete locally).
389    Future<void> declineGroupInvite(Uint8List groupId) async {
390      _ensureInitialized();
391      await rust_api.declineGroupInvite(groupId: groupId.toList());
392    }
393  
394    /// Get conversation messages for a group.
395    List<rust_api.MessageInfo> getGroupConversation(Uint8List groupId) {
396      _ensureInitialized();
397      return rust_api.getGroupConversation(groupId: groupId.toList());
398    }
399  
400    /// Add a member to a group.
401    Future<void> addGroupMember(Uint8List groupId, Uint8List contactId) async {
402      _ensureInitialized();
403      await rust_api.addGroupMember(
404        groupId: groupId.toList(),
405        contactId: contactId.toList(),
406      );
407    }
408  
409    /// Remove a member from a group.
410    Future<void> removeGroupMember(Uint8List groupId, Uint8List contactId) async {
411      _ensureInitialized();
412      await rust_api.removeGroupMember(
413        groupId: groupId.toList(),
414        contactId: contactId.toList(),
415      );
416    }
417  
418    /// Leave a group.
419    Future<void> leaveGroup(Uint8List groupId) async {
420      _ensureInitialized();
421      await rust_api.leaveGroup(groupId: groupId.toList());
422    }
423  
424    /// Rename a group.
425    Future<void> renameGroup(Uint8List groupId, String newName) async {
426      _ensureInitialized();
427      await rust_api.renameGroup(
428        groupId: groupId.toList(),
429        newName: newName,
430      );
431    }
432  
433    /// Set group muted status.
434    void setGroupMuted(Uint8List groupId, bool isMuted) {
435      _ensureInitialized();
436      rust_api.setGroupMuted(groupId: groupId.toList(), isMuted: isMuted);
437    }
438  
439    /// Set group disappearing message duration.
440    void setGroupDisappearing(Uint8List groupId, int duration) {
441      _ensureInitialized();
442      rust_api.setGroupDisappearing(
443        groupId: groupId.toList(),
444        duration: BigInt.from(duration),
445      );
446    }
447  
448    // ===========================================================================
449    // ROTATING IDS (BLE Discovery)
450    // ===========================================================================
451  
452    /// Get my current rotating IDs for BLE advertising.
453    ///
454    /// Returns a list of 16-byte rotating IDs, one per contact.
455    Future<List<Uint8List>> getMyRotatingIds() async {
456      _ensureInitialized();
457      return rust_api.getMyRotatingIds();
458    }
459  
460    /// Check if a scanned rotating ID matches any contact.
461    ///
462    /// Returns the contact ID if found, null otherwise.
463    Future<Uint8List?> checkRotatingId(Uint8List scannedId) async {
464      _ensureInitialized();
465      return rust_api.checkRotatingId(scannedId: scannedId.toList());
466    }
467  
468    // ===========================================================================
469    // HANDSHAKE (Noise Protocol)
470    // ===========================================================================
471  
472    /// Create a handshake initiator for a contact.
473    Future<rust_api.HandshakeHandle> createHandshakeInitiator(
474      Uint8List contactId,
475    ) async {
476      _ensureInitialized();
477      return rust_api.createHandshakeInitiator(contactId: contactId.toList());
478    }
479  
480    /// Create an anonymous handshake initiator for gossip-only connections.
481    Future<rust_api.HandshakeHandle> createHandshakeInitiatorAnonymous() async {
482      _ensureInitialized();
483      return rust_api.createHandshakeInitiatorAnonymous();
484    }
485  
486    /// Create a handshake responder.
487    Future<rust_api.HandshakeHandle> createHandshakeResponder() async {
488      _ensureInitialized();
489      return rust_api.createHandshakeResponder();
490    }
491  
492    /// Generate handshake message.
493    Future<Uint8List> handshakeGenerate(rust_api.HandshakeHandle handle) async {
494      _ensureInitialized();
495      return rust_api.handshakeGenerate(handle: handle);
496    }
497  
498    /// Process received handshake message.
499    Future<rust_api.HandshakeResult> handshakeProcess(
500      rust_api.HandshakeHandle handle,
501      Uint8List data,
502    ) async {
503      _ensureInitialized();
504      return rust_api.handshakeProcess(handle: handle, data: data.toList());
505    }
506  
507    /// Cancel a handshake.
508    Future<void> handshakeCancel(rust_api.HandshakeHandle handle) async {
509      _ensureInitialized();
510      rust_api.handshakeCancel(handle: handle);
511    }
512  
513    /// Finalize a completed handshake directly into a gossip exchange.
514    /// All BLE connections use this path (gossip-only architecture).
515    rust_api.GossipExchangeHandle handshakeFinalizeForGossip(
516      rust_api.HandshakeHandle handle,
517    ) {
518      _ensureInitialized();
519      return rust_api.handshakeFinalizeForGossip(handle: handle);
520    }
521  
522    // ===========================================================================
523    // BLE GOSSIP EXCHANGE
524    // ===========================================================================
525  
526    /// Get the next gossip data to send over BLE.
527    Uint8List? gossipExchangeSend(rust_api.GossipExchangeHandle handle) {
528      _ensureInitialized();
529      return rust_api.gossipExchangeSend(handle: handle);
530    }
531  
532    /// Process encrypted gossip data received from BLE.
533    /// Returns true if the gossip exchange is now complete.
534    bool gossipExchangeReceive(
535      rust_api.GossipExchangeHandle handle,
536      Uint8List data,
537    ) {
538      _ensureInitialized();
539      return rust_api.gossipExchangeReceive(
540        handle: handle,
541        data: data.toList(),
542      );
543    }
544  
545    /// Finalize a gossip exchange and get results.
546    rust_api.GossipExchangeComplete gossipExchangeFinalize(
547      rust_api.GossipExchangeHandle handle,
548    ) {
549      _ensureInitialized();
550      return rust_api.gossipExchangeFinalize(handle: handle);
551    }
552  
553    /// Cancel a gossip exchange and clean up resources.
554    void gossipExchangeCancel(rust_api.GossipExchangeHandle handle) {
555      _ensureInitialized();
556      rust_api.gossipExchangeCancel(handle: handle);
557    }
558  
559    /// Finalize a gossip exchange but preserve the transport for reuse.
560    rust_api.GossipFinalizeKeepTransportResult gossipExchangeFinalizeKeepTransport(
561      rust_api.GossipExchangeHandle handle,
562    ) {
563      _ensureInitialized();
564      return rust_api.gossipExchangeFinalizeKeepTransport(handle: handle);
565    }
566  
567    /// Create a new gossip exchange round from a stored transport.
568    rust_api.GossipExchangeHandle gossipStartNewRound(BigInt transportHandle) {
569      _ensureInitialized();
570      return rust_api.gossipStartNewRound(transportHandle: transportHandle);
571    }
572  
573    /// Release a stored gossip transport (cleanup on disconnect).
574    void gossipTransportRelease(BigInt transportHandle) {
575      _ensureInitialized();
576      rust_api.gossipTransportRelease(transportHandle: transportHandle);
577    }
578  
579    // ===========================================================================
580    // DHT MESSAGING
581    // ===========================================================================
582  
583    /// Publish queued messages to DHT for a contact.
584    ///
585    /// Returns the number of messages published.
586    Future<int> publishToDht(Uint8List contactId) async {
587      _ensureInitialized();
588      final result = await rust_api.publishMessagesToDht(contactId: contactId.toList());
589      return result.publishedCount;
590    }
591  
592    /// Fetch messages from DHT for our public key.
593    ///
594    /// Returns messages and contacts with reaction updates.
595    Future<rust_api.DhtFetchResult> fetchFromDht() async {
596      _ensureInitialized();
597      return await rust_api.fetchMessagesFromDht();
598    }
599  
600    /// Acknowledge DHT messages (mark as received).
601    Future<void> acknowledgeDhtMessages(List<Uint8List> messageIds) async {
602      _ensureInitialized();
603      rust_api.acknowledgeDhtMessages(messageIds: messageIds);
604    }
605  
606    // ===========================================================================
607    // TOR P2P RENDEZVOUS (DHT)
608    // ===========================================================================
609  
610    /// Publish our .onion address to the DHT for peer discovery.
611    Future<void> publishOnionAddress(String onionAddress) async {
612      _ensureInitialized();
613      await rust_api.publishOnionAddress(onionAddress: onionAddress);
614    }
615  
616    /// Fetch a contact's .onion address from the DHT.
617    /// Returns null if not found or stale (>1 hour old).
618    Future<String?> fetchContactOnionAddress(Uint8List contactId) async {
619      _ensureInitialized();
620      return await rust_api.fetchContactOnionAddress(
621          contactId: contactId.toList());
622    }
623  
624    /// Get all cached onion addresses for contacts.
625    /// Used on startup to connect instantly without waiting for DHT rendezvous.
626    Future<List<rust_api.CachedOnionAddress>> getCachedOnionAddresses() async {
627      _ensureInitialized();
628      return rust_api.getCachedOnionAddresses();
629    }
630  
631    // ===========================================================================
632    // TOR P2P MESSAGING
633    // ===========================================================================
634  
635    /// Get queued encrypted messages ready for P2P delivery to a contact.
636    Future<List<rust_api.EncryptedPayload>> getQueuedEncrypted(
637        Uint8List contactId) async {
638      _ensureInitialized();
639      return rust_api.getQueuedEncrypted(contactId: contactId.toList());
640    }
641  
642    /// Re-queue any Failed messages for a contact back to Queued state.
643    Future<int> requeueFailedMessages(Uint8List contactId) async {
644      _ensureInitialized();
645      return rust_api.requeueFailedMessages(contactId: contactId.toList());
646    }
647  
648    /// Queue an outbound message for BLE gossip mesh relay.
649    /// Returns true if successfully queued, false if not applicable.
650    bool queueForGossipRelay(Uint8List messageId) {
651      _ensureInitialized();
652      return rust_api.queueForGossipRelay(messageId: messageId.toList());
653    }
654  
655    /// Mark messages as delivered via Tor P2P transport.
656    Future<void> markDeliveredViaTor(List<Uint8List> messageIds) async {
657      _ensureInitialized();
658      await rust_api.markDeliveredViaTor(messageIds: messageIds);
659    }
660  
661    /// Process an encrypted message received via Tor P2P.
662    /// Returns the message info if successfully processed (null for reactions/chunks).
663    Future<rust_api.MessageInfo?> processTorMessage(
664        Uint8List senderIdentityKey, Uint8List payload) async {
665      _ensureInitialized();
666      return rust_api.processTorMessage(
667        senderIdentityKey: senderIdentityKey.toList(),
668        payload: payload.toList(),
669      );
670    }
671  
672    /// Get a contact's identity public key (ed25519, 32 bytes).
673    Uint8List getContactIdentityKey(Uint8List contactId) {
674      _ensureInitialized();
675      return rust_api.getContactIdentityKey(contactId: contactId.toList());
676    }
677  
678    /// Look up a contact's ID by their identity public key (ed25519).
679    Future<Uint8List?> getContactIdByIdentityKey(Uint8List identityKey) async {
680      _ensureInitialized();
681      final result = await rust_api.getContactIdByIdentityKey(
682        identityKey: identityKey.toList(),
683      );
684      return result != null ? Uint8List.fromList(result) : null;
685    }
686  
687    // ===========================================================================
688    // IROH P2P TRANSPORT
689    // ===========================================================================
690  
691    /// Get the current state of the iroh transport.
692    rust_api.IrohStateDto getIrohState() {
693      _ensureInitialized();
694      return rust_api.getIrohState();
695    }
696  
697    /// Start the iroh P2P transport.
698    Future<void> startIroh() async {
699      _ensureInitialized();
700      await rust_api.startIroh();
701    }
702  
703    /// Stop the iroh P2P transport.
704    Future<void> stopIroh() async {
705      _ensureInitialized();
706      await rust_api.stopIroh();
707    }
708  
709    /// Publish our iroh EndpointId to the DHT for peer discovery.
710    Future<void> publishIrohEndpointId() async {
711      _ensureInitialized();
712      await rust_api.publishIrohEndpointId();
713    }
714  
715    /// Fetch a contact's iroh EndpointId from the DHT.
716    Future<String?> fetchContactIrohEndpointId(Uint8List contactId) async {
717      _ensureInitialized();
718      return await rust_api.fetchContactIrohEndpointId(
719          contactId: contactId.toList());
720    }
721  
722    /// Send an encrypted payload to a peer via iroh.
723    Future<void> sendViaIroh(String endpointId, Uint8List payload) async {
724      _ensureInitialized();
725      await rust_api.sendViaIroh(
726          endpointId: endpointId, payload: payload.toList());
727    }
728  
729    /// Process an encrypted message received via iroh P2P.
730    Future<rust_api.MessageInfo?> processIrohMessage(
731        Uint8List senderIdentityKey, Uint8List payload) async {
732      _ensureInitialized();
733      return rust_api.processIrohMessage(
734        senderIdentityKey: senderIdentityKey.toList(),
735        payload: payload.toList(),
736      );
737    }
738  
739    /// Process an iroh message when the sender's endpoint ID is unknown.
740    Future<rust_api.MessageInfo?> processIrohMessageUnknownSender({
741      required Uint8List payload,
742    }) async {
743      _ensureInitialized();
744      return rust_api.processIrohMessageUnknownSender(
745        payload: payload.toList(),
746      );
747    }
748  
749    /// Poll for incoming iroh messages (non-blocking).
750    Future<rust_api.IrohIncomingMessage?> pollIrohIncoming() async {
751      _ensureInitialized();
752      return rust_api.pollIrohIncoming();
753    }
754  
755    /// Get all cached iroh endpoint IDs for contacts.
756    Future<List<rust_api.CachedIrohEndpoint>> getCachedIrohEndpointIds() async {
757      _ensureInitialized();
758      return rust_api.getCachedIrohEndpointIds();
759    }
760  
761    /// Manually set a contact's iroh endpoint ID (bypasses DHT).
762    void setContactIrohEndpoint(Uint8List contactId, String endpointId) {
763      _ensureInitialized();
764      rust_api.setContactIrohEndpoint(
765        contactId: contactId.toList(),
766        endpointId: endpointId,
767      );
768    }
769  
770    // ===========================================================================
771    // MAINTENANCE
772    // ===========================================================================
773  
774    /// Run maintenance tasks (cleanup expired messages, etc.)
775    Future<void> runMaintenance() async {
776      _ensureInitialized();
777      rust_api.runMaintenance();
778    }
779  
780    /// Get database statistics.
781    Future<String> getStatistics() async {
782      _ensureInitialized();
783      return rust_api.getStatistics();
784    }
785  
786    // ===========================================================================
787    // PRIVATE HELPERS
788    // ===========================================================================
789  
790    void _ensureInitialized() {
791      if (!_initialized) {
792        throw StateError(
793          'DeadDropService not initialized. Call initialize() first.',
794        );
795      }
796    }
797  }
798  
799  // =============================================================================
800  // HELPER EXTENSIONS
801  // =============================================================================
802  
803  /// Extension to get display name from ContactInfo
804  extension ContactInfoDisplay on rust_api.ContactInfo {
805    /// Display name (nickname or fingerprint)
806    String get displayName => nickname ?? fingerprint;
807  }
808  
809  /// Contact state constants
810  class ContactState {
811    static const int pending = 0;
812    static const int active = 1;
813    static const int blocked = 2;
814    static const int revoked = 3;
815  }
816  
817  /// Message content type constants (matches Rust ContentType enum)
818  class ContentType {
819    static const int text = 0;
820    static const int document = 1;
821    static const int reaction = 2;
822    static const int documentChunk = 3;
823    static const int deliveryReceipt = 4;
824    static const int groupManagement = 5;
825  }
826  
827  /// Message state constants
828  class MessageState {
829    static const int pending = 0;
830    static const int sent = 1;
831    static const int delivered = 2;
832    static const int failed = 3;
833  }