@magicred-1/ble-mesh 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1838 @@
1
+ import Foundation
2
+ import CoreBluetooth
3
+ import CryptoKit
4
+
5
+ @objc(BleMesh)
6
+ class BleMesh: RCTEventEmitter {
7
+
8
+ // MARK: - Constants
9
+
10
+ #if DEBUG
11
+ static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A")
12
+ #else
13
+ static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
14
+ #endif
15
+ static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
16
+
17
+ private let messageTTL: UInt8 = 7
18
+
19
+ // MARK: - BLE Objects
20
+
21
+ private var centralManager: CBCentralManager?
22
+ private var peripheralManager: CBPeripheralManager?
23
+ private var characteristic: CBMutableCharacteristic?
24
+
25
+ // MARK: - State
26
+
27
+ private var isRunning = false
28
+ private var myNickname: String = "anon"
29
+ private var myPeerID: String = ""
30
+ private var myPeerIDData: Data = Data()
31
+
32
+ // Peer tracking
33
+ private struct PeerInfo {
34
+ let peerId: String
35
+ var nickname: String
36
+ var isConnected: Bool
37
+ var rssi: Int?
38
+ var lastSeen: Date
39
+ var noisePublicKey: Data?
40
+ var isVerified: Bool
41
+ }
42
+ private var peers: [String: PeerInfo] = [:]
43
+ private var peripherals: [String: CBPeripheral] = [:]
44
+ private var peripheralToPeer: [String: String] = [:]
45
+ private var subscribedCentrals: [CBCentral] = []
46
+
47
+ // Encryption
48
+ private var privateKey: Curve25519.KeyAgreement.PrivateKey?
49
+ private var signingKey: Curve25519.Signing.PrivateKey?
50
+ private var sessions: [String: Data] = [:]
51
+
52
+ // Message deduplication
53
+ private var processedMessages: Set<String> = []
54
+
55
+ // File transfer tracking
56
+ private struct FileTransfer {
57
+ let id: String
58
+ let fileName: String
59
+ let fileSize: Int
60
+ let mimeType: String
61
+ let senderPeerId: String
62
+ let totalChunks: Int
63
+ var receivedChunks: Set<Int> = []
64
+ }
65
+ private var activeFileTransfers: [String: FileTransfer] = [:]
66
+ private var fileTransferFragments: [String: [Int: Data]] = [:]
67
+ private let fileFragmentSize = 180 // Max payload size for fragments
68
+
69
+ // Transaction chunking tracking
70
+ private struct TransactionChunks {
71
+ let txId: String
72
+ let senderId: String
73
+ let totalSize: Int
74
+ let totalChunks: Int
75
+ var chunks: [Int: Data] = [:]
76
+ }
77
+ private var pendingTransactionChunks: [String: TransactionChunks] = [:]
78
+
79
+ // Queues
80
+ private let bleQueue = DispatchQueue(label: "mesh.bluetooth", qos: .userInitiated)
81
+ private let messageQueue = DispatchQueue(label: "mesh.message", attributes: .concurrent)
82
+
83
+ // MARK: - RCTEventEmitter
84
+
85
+ override init() {
86
+ super.init()
87
+ generateIdentity()
88
+ }
89
+
90
+ override static func moduleName() -> String! {
91
+ return "BleMesh"
92
+ }
93
+
94
+ override func supportedEvents() -> [String]! {
95
+ return [
96
+ "onPeerListUpdated",
97
+ "onMessageReceived",
98
+ "onFileReceived",
99
+ "onTransactionReceived",
100
+ "onTransactionResponse",
101
+ "onConnectionStateChanged",
102
+ "onReadReceipt",
103
+ "onDeliveryAck",
104
+ "onError"
105
+ ]
106
+ }
107
+
108
+ override static func requiresMainQueueSetup() -> Bool {
109
+ return false
110
+ }
111
+
112
+ // MARK: - Identity
113
+
114
+ private func generateIdentity() {
115
+ // Generate or load keys
116
+ if let savedPrivateKey = loadKey(forKey: "mesh.privateKey") {
117
+ privateKey = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: savedPrivateKey)
118
+ }
119
+ if privateKey == nil {
120
+ privateKey = Curve25519.KeyAgreement.PrivateKey()
121
+ if let pk = privateKey {
122
+ saveKey(pk.rawRepresentation, forKey: "mesh.privateKey")
123
+ }
124
+ }
125
+
126
+ if let savedSigningKey = loadKey(forKey: "mesh.signingKey") {
127
+ signingKey = try? Curve25519.Signing.PrivateKey(rawRepresentation: savedSigningKey)
128
+ }
129
+ if signingKey == nil {
130
+ signingKey = Curve25519.Signing.PrivateKey()
131
+ if let sk = signingKey {
132
+ saveKey(sk.rawRepresentation, forKey: "mesh.signingKey")
133
+ }
134
+ }
135
+
136
+ // Generate peer ID from public key fingerprint (first 16 hex chars of SHA256)
137
+ if let pk = privateKey {
138
+ let fingerprint = SHA256.hash(data: pk.publicKey.rawRepresentation)
139
+ myPeerID = fingerprint.prefix(8).map { String(format: "%02x", $0) }.joined()
140
+ myPeerIDData = Data(fingerprint.prefix(8))
141
+ }
142
+ }
143
+
144
+ private func loadKey(forKey key: String) -> Data? {
145
+ let query: [String: Any] = [
146
+ kSecClass as String: kSecClassGenericPassword,
147
+ kSecAttrService as String: "com.blemesh",
148
+ kSecAttrAccount as String: key,
149
+ kSecReturnData as String: true
150
+ ]
151
+
152
+ var result: AnyObject?
153
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
154
+
155
+ if status == errSecSuccess {
156
+ return result as? Data
157
+ }
158
+ return nil
159
+ }
160
+
161
+ private func saveKey(_ data: Data, forKey key: String) {
162
+ let query: [String: Any] = [
163
+ kSecClass as String: kSecClassGenericPassword,
164
+ kSecAttrService as String: "com.blemesh",
165
+ kSecAttrAccount as String: key,
166
+ kSecValueData as String: data
167
+ ]
168
+
169
+ SecItemDelete(query as CFDictionary)
170
+ SecItemAdd(query as CFDictionary, nil)
171
+ }
172
+
173
+ // MARK: - React Native API
174
+
175
+ @objc(requestPermissions:rejecter:)
176
+ func requestPermissions(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
177
+ // On iOS, BLE permissions are requested when CBCentralManager is initialized
178
+ // We need to check the current state
179
+ let tempManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false])
180
+
181
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
182
+ let bluetoothGranted = tempManager.state != .unauthorized
183
+ resolve([
184
+ "bluetooth": bluetoothGranted,
185
+ "location": true // iOS doesn't require location for BLE
186
+ ])
187
+ }
188
+ }
189
+
190
+ @objc(checkPermissions:rejecter:)
191
+ func checkPermissions(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
192
+ let state = centralManager?.state ?? .unknown
193
+ let bluetoothGranted = state != .unauthorized
194
+
195
+ resolve([
196
+ "bluetooth": bluetoothGranted,
197
+ "location": true
198
+ ])
199
+ }
200
+
201
+ @objc(start:resolver:rejecter:)
202
+ func start(_ nickname: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
203
+ myNickname = nickname
204
+
205
+ bleQueue.async { [weak self] in
206
+ guard let self = self else { return }
207
+
208
+ self.centralManager = CBCentralManager(delegate: self, queue: self.bleQueue)
209
+ self.peripheralManager = CBPeripheralManager(delegate: self, queue: self.bleQueue)
210
+
211
+ self.isRunning = true
212
+
213
+ DispatchQueue.main.async {
214
+ resolve(nil)
215
+ }
216
+ }
217
+ }
218
+
219
+ @objc(stop:rejecter:)
220
+ func stop(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
221
+ bleQueue.async { [weak self] in
222
+ guard let self = self else { return }
223
+
224
+ // Send leave announcement
225
+ self.sendLeaveAnnouncement()
226
+
227
+ // Stop scanning and advertising
228
+ self.centralManager?.stopScan()
229
+ self.peripheralManager?.stopAdvertising()
230
+
231
+ // Disconnect all peripherals
232
+ for (_, peripheral) in self.peripherals {
233
+ self.centralManager?.cancelPeripheralConnection(peripheral)
234
+ }
235
+
236
+ self.isRunning = false
237
+ self.peers.removeAll()
238
+ self.peripherals.removeAll()
239
+ self.peripheralToPeer.removeAll()
240
+ self.subscribedCentrals.removeAll()
241
+
242
+ DispatchQueue.main.async {
243
+ resolve(nil)
244
+ }
245
+ }
246
+ }
247
+
248
+ @objc(setNickname:resolver:rejecter:)
249
+ func setNickname(_ nickname: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
250
+ myNickname = nickname
251
+ sendAnnounce()
252
+ resolve(nil)
253
+ }
254
+
255
+ @objc(getMyPeerId:rejecter:)
256
+ func getMyPeerId(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
257
+ resolve(myPeerID)
258
+ }
259
+
260
+ @objc(getMyNickname:rejecter:)
261
+ func getMyNickname(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
262
+ resolve(myNickname)
263
+ }
264
+
265
+ @objc(getPeers:rejecter:)
266
+ func getPeers(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
267
+ let peerList = peers.values.map { peer in
268
+ return [
269
+ "peerId": peer.peerId,
270
+ "nickname": peer.nickname,
271
+ "isConnected": peer.isConnected,
272
+ "rssi": peer.rssi ?? NSNull(),
273
+ "lastSeen": Int(peer.lastSeen.timeIntervalSince1970 * 1000),
274
+ "isVerified": peer.isVerified
275
+ ] as [String: Any]
276
+ }
277
+ resolve(peerList)
278
+ }
279
+
280
+ @objc(sendMessage:channel:resolver:rejecter:)
281
+ func sendMessage(_ content: String, channel: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
282
+ let messageId = UUID().uuidString
283
+
284
+ messageQueue.async { [weak self] in
285
+ guard let self = self else { return }
286
+
287
+ let packet = self.createPacket(
288
+ type: MessageType.message.rawValue,
289
+ payload: Data(content.utf8),
290
+ recipientID: nil
291
+ )
292
+
293
+ self.broadcastPacket(packet)
294
+
295
+ DispatchQueue.main.async {
296
+ resolve(messageId)
297
+ }
298
+ }
299
+ }
300
+
301
+ @objc(sendPrivateMessage:recipientPeerId:resolver:rejecter:)
302
+ func sendPrivateMessage(_ content: String, recipientPeerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
303
+ let messageId = UUID().uuidString
304
+
305
+ messageQueue.async { [weak self] in
306
+ guard let self = self else { return }
307
+
308
+ // Check if we have an encryption session
309
+ if self.sessions[recipientPeerId] != nil {
310
+ // Encrypt the message
311
+ if let encrypted = self.encryptMessage(content, for: recipientPeerId) {
312
+ let packet = self.createPacket(
313
+ type: MessageType.noiseEncrypted.rawValue,
314
+ payload: encrypted,
315
+ recipientID: Data(hexString: recipientPeerId)
316
+ )
317
+ self.broadcastPacket(packet)
318
+ }
319
+ } else {
320
+ // Initiate handshake first
321
+ self.initiateHandshakeInternal(with: recipientPeerId)
322
+ }
323
+
324
+ DispatchQueue.main.async {
325
+ resolve(messageId)
326
+ }
327
+ }
328
+ }
329
+
330
+ @objc(sendFile:recipientPeerId:channel:resolver:rejecter:)
331
+ func sendFile(_ filePath: String, recipientPeerId: String?, channel: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
332
+ let transferId = UUID().uuidString
333
+
334
+ guard let fileData = FileManager.default.contents(atPath: filePath) else {
335
+ reject("FILE_ERROR", "Could not read file at path: \(filePath)", nil)
336
+ return
337
+ }
338
+
339
+ let fileName = (filePath as NSString).lastPathComponent
340
+ let mimeType = getMimeType(for: fileName)
341
+ let totalChunks = (fileData.count + fileFragmentSize - 1) / fileFragmentSize
342
+
343
+ messageQueue.async { [weak self] in
344
+ guard let self = self else { return }
345
+
346
+ // Build and send metadata packet
347
+ let metadataPayload = self.buildFileTransferMetadata(
348
+ transferId: transferId,
349
+ fileName: fileName,
350
+ fileSize: fileData.count,
351
+ mimeType: mimeType,
352
+ totalChunks: totalChunks
353
+ )
354
+
355
+ let metadataPacket = self.createPacket(
356
+ type: MessageType.fileTransfer.rawValue,
357
+ payload: metadataPayload,
358
+ recipientID: recipientPeerId != nil ? Data(hexString: recipientPeerId!) : nil
359
+ )
360
+ self.broadcastPacket(metadataPacket)
361
+
362
+ // Send file fragments
363
+ for chunkIndex in 0..<totalChunks {
364
+ let start = chunkIndex * self.fileFragmentSize
365
+ let end = min(start + self.fileFragmentSize, fileData.count)
366
+ let chunkData = fileData.subdata(in: start..<end)
367
+
368
+ let fragmentPayload = self.buildFileFragment(
369
+ transferId: transferId,
370
+ chunkIndex: chunkIndex,
371
+ totalChunks: totalChunks,
372
+ chunkData: chunkData
373
+ )
374
+
375
+ let fragmentPacket = self.createPacket(
376
+ type: MessageType.fragment.rawValue,
377
+ payload: fragmentPayload,
378
+ recipientID: recipientPeerId != nil ? Data(hexString: recipientPeerId!) : nil
379
+ )
380
+ self.broadcastPacket(fragmentPacket)
381
+
382
+ // Small delay to avoid overwhelming the BLE stack
383
+ Thread.sleep(forTimeInterval: 0.05)
384
+ }
385
+
386
+ DispatchQueue.main.async {
387
+ resolve(transferId)
388
+ }
389
+ }
390
+ }
391
+
392
+ private func buildFileTransferMetadata(transferId: String, fileName: String, fileSize: Int, mimeType: String, totalChunks: Int) -> Data {
393
+ var payload = Data()
394
+
395
+ // Transfer ID TLV (tag 0x01)
396
+ let transferIdData = Data(transferId.utf8)
397
+ payload.append(0x01)
398
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(transferIdData.count).bigEndian) { Array($0) })
399
+ payload.append(transferIdData)
400
+
401
+ // File name TLV (tag 0x02)
402
+ let fileNameData = Data(fileName.utf8)
403
+ payload.append(0x02)
404
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(fileNameData.count).bigEndian) { Array($0) })
405
+ payload.append(fileNameData)
406
+
407
+ // File size TLV (tag 0x03) - 4 bytes
408
+ payload.append(0x03)
409
+ payload.append(0x00)
410
+ payload.append(0x04)
411
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(fileSize).bigEndian) { Array($0) })
412
+
413
+ // MIME type TLV (tag 0x04)
414
+ let mimeTypeData = Data(mimeType.utf8)
415
+ payload.append(0x04)
416
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(mimeTypeData.count).bigEndian) { Array($0) })
417
+ payload.append(mimeTypeData)
418
+
419
+ // Total chunks TLV (tag 0x05) - 4 bytes
420
+ payload.append(0x05)
421
+ payload.append(0x00)
422
+ payload.append(0x04)
423
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
424
+
425
+ return payload
426
+ }
427
+
428
+ private func buildFileFragment(transferId: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) -> Data {
429
+ var payload = Data()
430
+
431
+ // Transfer ID TLV (tag 0x01)
432
+ let transferIdData = Data(transferId.utf8)
433
+ payload.append(0x01)
434
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(transferIdData.count).bigEndian) { Array($0) })
435
+ payload.append(transferIdData)
436
+
437
+ // Chunk index TLV (tag 0x02) - 4 bytes
438
+ payload.append(0x02)
439
+ payload.append(0x00)
440
+ payload.append(0x04)
441
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(chunkIndex).bigEndian) { Array($0) })
442
+
443
+ // Total chunks TLV (tag 0x03) - 4 bytes
444
+ payload.append(0x03)
445
+ payload.append(0x00)
446
+ payload.append(0x04)
447
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
448
+
449
+ // Chunk data TLV (tag 0x04)
450
+ payload.append(0x04)
451
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(chunkData.count).bigEndian) { Array($0) })
452
+ payload.append(chunkData)
453
+
454
+ return payload
455
+ }
456
+
457
+ @objc(sendTransaction:serializedTransaction:recipientPeerId:firstSignerPublicKey:secondSignerPublicKey:description:resolver:rejecter:)
458
+ func sendTransaction(
459
+ _ txId: String,
460
+ serializedTransaction: String,
461
+ recipientPeerId: String?,
462
+ firstSignerPublicKey: String,
463
+ secondSignerPublicKey: String?,
464
+ description: String?,
465
+ resolver resolve: @escaping RCTPromiseResolveBlock,
466
+ rejecter reject: @escaping RCTPromiseRejectBlock
467
+ ) {
468
+ messageQueue.async { [weak self] in
469
+ guard let self = self else { return }
470
+
471
+ // Build TLV payload for Solana transaction
472
+ let payload = self.buildSolanaTransactionPayload(
473
+ txId: txId,
474
+ serializedTransaction: serializedTransaction,
475
+ firstSignerPublicKey: firstSignerPublicKey,
476
+ secondSignerPublicKey: secondSignerPublicKey,
477
+ description: description
478
+ )
479
+
480
+ // Check if we need chunking (MTU limit is ~500 bytes for encrypted payload)
481
+ let maxPayloadSize = 450 // Conservative limit
482
+
483
+ if let targetPeerId = recipientPeerId {
484
+ // Send to specific peer
485
+ self.sendTransactionToPeer(txId: txId, payload: payload, recipientPeerId: targetPeerId, maxPayloadSize: maxPayloadSize)
486
+ } else {
487
+ // Broadcast to all connected peers with sessions
488
+ print("Broadcasting transaction \(txId) to all peers")
489
+ var sentCount = 0
490
+ for peerId in self.sessions.keys {
491
+ self.sendTransactionToPeer(txId: txId, payload: payload, recipientPeerId: peerId, maxPayloadSize: maxPayloadSize)
492
+ sentCount += 1
493
+ }
494
+ print("Transaction broadcast to \(sentCount) peers")
495
+ }
496
+
497
+ DispatchQueue.main.async {
498
+ resolve(txId)
499
+ }
500
+ }
501
+ }
502
+
503
+ private func sendTransactionToPeer(txId: String, payload: Data, recipientPeerId: String, maxPayloadSize: Int) {
504
+ guard self.sessions[recipientPeerId] != nil else {
505
+ // No session, initiate handshake first
506
+ self.initiateHandshakeInternal(with: recipientPeerId)
507
+ return
508
+ }
509
+
510
+ var encryptedPayload = Data([NoisePayloadType.solanaTransaction.rawValue])
511
+ encryptedPayload.append(payload)
512
+
513
+ if let encrypted = self.encryptPayload(encryptedPayload, for: recipientPeerId) {
514
+ if encrypted.count <= maxPayloadSize {
515
+ // Small enough for single packet
516
+ let packet = self.createPacket(
517
+ type: MessageType.noiseEncrypted.rawValue,
518
+ payload: encrypted,
519
+ recipientID: Data(hexString: recipientPeerId)
520
+ )
521
+ self.broadcastPacket(packet)
522
+ } else {
523
+ // Need to chunk large transaction
524
+ print("Transaction too large (\(encrypted.count) bytes), using chunking")
525
+ self.sendChunkedTransaction(txId: txId, encryptedData: encrypted, recipientPeerId: recipientPeerId)
526
+ }
527
+ }
528
+ }
529
+
530
+ private func sendChunkedTransaction(txId: String, encryptedData: Data, recipientPeerId: String) {
531
+ let chunkSize = 400 // Max chunk size
532
+ let totalChunks = (encryptedData.count + chunkSize - 1) / chunkSize
533
+
534
+ print("Sending chunked transaction \(txId): \(encryptedData.count) bytes in \(totalChunks) chunks")
535
+
536
+ // Send metadata packet first
537
+ let metadataPayload = self.buildTransactionChunkMetadata(txId: txId, totalSize: encryptedData.count, totalChunks: totalChunks)
538
+ let metadataPacket = self.createPacket(
539
+ type: MessageType.solanaTransaction.rawValue,
540
+ payload: metadataPayload,
541
+ recipientID: Data(hexString: recipientPeerId)
542
+ )
543
+ self.broadcastPacket(metadataPacket)
544
+
545
+ // Small delay to let receiver prepare
546
+ Thread.sleep(forTimeInterval: 0.1)
547
+
548
+ // Send chunks
549
+ for chunkIndex in 0..<totalChunks {
550
+ let start = chunkIndex * chunkSize
551
+ let end = min(start + chunkSize, encryptedData.count)
552
+ let chunkData = encryptedData.subdata(in: start..<end)
553
+
554
+ let chunkPayload = self.buildTransactionChunk(txId: txId, chunkIndex: chunkIndex, totalChunks: totalChunks, chunkData: chunkData)
555
+ let chunkPacket = self.createPacket(
556
+ type: MessageType.fragment.rawValue,
557
+ payload: chunkPayload,
558
+ recipientID: Data(hexString: recipientPeerId)
559
+ )
560
+ self.broadcastPacket(chunkPacket)
561
+
562
+ print("Sent chunk \(chunkIndex + 1)/\(totalChunks) for transaction \(txId)")
563
+ Thread.sleep(forTimeInterval: 0.05) // Small delay between chunks
564
+ }
565
+ }
566
+
567
+ private func buildTransactionChunkMetadata(txId: String, totalSize: Int, totalChunks: Int) -> Data {
568
+ var payload = Data()
569
+
570
+ // Transaction ID TLV (tag 0x01)
571
+ let txIdData = Data(txId.utf8)
572
+ payload.append(0x01)
573
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
574
+ payload.append(txIdData)
575
+
576
+ // Total size TLV (tag 0x02) - 4 bytes
577
+ payload.append(0x02)
578
+ payload.append(0x00)
579
+ payload.append(0x04)
580
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalSize).bigEndian) { Array($0) })
581
+
582
+ // Total chunks TLV (tag 0x03) - 4 bytes
583
+ payload.append(0x03)
584
+ payload.append(0x00)
585
+ payload.append(0x04)
586
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
587
+
588
+ return payload
589
+ }
590
+
591
+ private func buildTransactionChunk(txId: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) -> Data {
592
+ var payload = Data()
593
+
594
+ // Transaction ID TLV (tag 0x01)
595
+ let txIdData = Data(txId.utf8)
596
+ payload.append(0x01)
597
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
598
+ payload.append(txIdData)
599
+
600
+ // Chunk index TLV (tag 0x02) - 4 bytes
601
+ payload.append(0x02)
602
+ payload.append(0x00)
603
+ payload.append(0x04)
604
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(chunkIndex).bigEndian) { Array($0) })
605
+
606
+ // Total chunks TLV (tag 0x03) - 4 bytes
607
+ payload.append(0x03)
608
+ payload.append(0x00)
609
+ payload.append(0x04)
610
+ payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
611
+
612
+ // Chunk data TLV (tag 0x04)
613
+ payload.append(0x04)
614
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(chunkData.count).bigEndian) { Array($0) })
615
+ payload.append(chunkData)
616
+
617
+ return payload
618
+ }
619
+
620
+ private func buildSolanaTransactionPayload(
621
+ txId: String,
622
+ serializedTransaction: String,
623
+ firstSignerPublicKey: String,
624
+ secondSignerPublicKey: String?,
625
+ description: String?
626
+ ) -> Data {
627
+ var payload = Data()
628
+
629
+ // Transaction ID TLV (tag 0x01)
630
+ let txIdData = Data(txId.utf8)
631
+ payload.append(0x01)
632
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
633
+ payload.append(txIdData)
634
+
635
+ // Serialized transaction TLV (tag 0x02)
636
+ let txData = Data(serializedTransaction.utf8)
637
+ payload.append(0x02)
638
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(txData.count).bigEndian) { Array($0) })
639
+ payload.append(txData)
640
+
641
+ // First signer public key TLV (tag 0x03)
642
+ let firstSignerData = Data(firstSignerPublicKey.utf8)
643
+ payload.append(0x03)
644
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(firstSignerData.count).bigEndian) { Array($0) })
645
+ payload.append(firstSignerData)
646
+
647
+ // Second signer public key TLV (tag 0x04) - optional
648
+ if let secondSigner = secondSignerPublicKey, !secondSigner.isEmpty {
649
+ let secondSignerData = Data(secondSigner.utf8)
650
+ payload.append(0x04)
651
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(secondSignerData.count).bigEndian) { Array($0) })
652
+ payload.append(secondSignerData)
653
+ }
654
+
655
+ // Description TLV (tag 0x05) - optional
656
+ if let desc = description, !desc.isEmpty {
657
+ let descData = Data(desc.utf8)
658
+ payload.append(0x05)
659
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(descData.count).bigEndian) { Array($0) })
660
+ payload.append(descData)
661
+ }
662
+
663
+ return payload
664
+ }
665
+
666
+ @objc(respondToTransaction:recipientPeerId:signedTransaction:error:resolver:rejecter:)
667
+ func respondToTransaction(
668
+ _ transactionId: String,
669
+ recipientPeerId: String,
670
+ signedTransaction: String?,
671
+ error: String?,
672
+ resolver resolve: @escaping RCTPromiseResolveBlock,
673
+ rejecter reject: @escaping RCTPromiseRejectBlock
674
+ ) {
675
+ messageQueue.async { [weak self] in
676
+ guard let self = self else { return }
677
+
678
+ // Build TLV payload for response
679
+ var payload = Data()
680
+
681
+ // Transaction ID TLV (tag 0x01)
682
+ let txIdData = Data(transactionId.utf8)
683
+ payload.append(0x01)
684
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
685
+ payload.append(txIdData)
686
+
687
+ // Signed transaction TLV (tag 0x02) - optional
688
+ if let signedTx = signedTransaction, !signedTx.isEmpty {
689
+ let signedTxData = Data(signedTx.utf8)
690
+ payload.append(0x02)
691
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(signedTxData.count).bigEndian) { Array($0) })
692
+ payload.append(signedTxData)
693
+ }
694
+
695
+ // Error TLV (tag 0x03) - optional
696
+ if let err = error, !err.isEmpty {
697
+ let errData = Data(err.utf8)
698
+ payload.append(0x03)
699
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(errData.count).bigEndian) { Array($0) })
700
+ payload.append(errData)
701
+ }
702
+
703
+ var encryptedPayload = Data([NoisePayloadType.transactionResponse.rawValue])
704
+ encryptedPayload.append(payload)
705
+
706
+ // Send encrypted response
707
+ if let encrypted = self.encryptPayload(encryptedPayload, for: recipientPeerId) {
708
+ let packet = self.createPacket(
709
+ type: MessageType.noiseEncrypted.rawValue,
710
+ payload: encrypted,
711
+ recipientID: Data(hexString: recipientPeerId)
712
+ )
713
+ self.broadcastPacket(packet)
714
+ }
715
+
716
+ DispatchQueue.main.async {
717
+ resolve(nil)
718
+ }
719
+ }
720
+ }
721
+
722
+ @objc(sendReadReceipt:recipientPeerId:resolver:rejecter:)
723
+ func sendReadReceipt(_ messageId: String, recipientPeerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
724
+ messageQueue.async { [weak self] in
725
+ guard let self = self else { return }
726
+
727
+ // Create read receipt payload
728
+ var payload = Data([NoisePayloadType.readReceipt.rawValue])
729
+ payload.append(contentsOf: messageId.utf8)
730
+
731
+ if let encrypted = self.encryptPayload(payload, for: recipientPeerId) {
732
+ let packet = self.createPacket(
733
+ type: MessageType.noiseEncrypted.rawValue,
734
+ payload: encrypted,
735
+ recipientID: Data(hexString: recipientPeerId)
736
+ )
737
+ self.broadcastPacket(packet)
738
+ }
739
+
740
+ DispatchQueue.main.async {
741
+ resolve(nil)
742
+ }
743
+ }
744
+ }
745
+
746
+ @objc(hasEncryptedSession:resolver:rejecter:)
747
+ func hasEncryptedSession(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
748
+ resolve(sessions[peerId] != nil)
749
+ }
750
+
751
+ @objc(initiateHandshake:resolver:rejecter:)
752
+ func initiateHandshake(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
753
+ initiateHandshakeInternal(with: peerId)
754
+ resolve(nil)
755
+ }
756
+
757
+ @objc(getIdentityFingerprint:rejecter:)
758
+ func getIdentityFingerprint(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
759
+ if let pk = privateKey {
760
+ let fingerprint = SHA256.hash(data: pk.publicKey.rawRepresentation)
761
+ let fingerprintHex = fingerprint.map { String(format: "%02x", $0) }.joined()
762
+ resolve(fingerprintHex)
763
+ } else {
764
+ resolve(nil)
765
+ }
766
+ }
767
+
768
+ @objc(getPeerFingerprint:resolver:rejecter:)
769
+ func getPeerFingerprint(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
770
+ if let peer = peers[peerId], let publicKey = peer.noisePublicKey {
771
+ let fingerprint = SHA256.hash(data: publicKey)
772
+ let fingerprintHex = fingerprint.map { String(format: "%02x", $0) }.joined()
773
+ resolve(fingerprintHex)
774
+ } else {
775
+ resolve(nil)
776
+ }
777
+ }
778
+
779
+ @objc(broadcastAnnounce:rejecter:)
780
+ func broadcastAnnounce(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
781
+ sendAnnounce()
782
+ resolve(nil)
783
+ }
784
+
785
+ // MARK: - Private Methods
786
+
787
+ private func sendAnnounce() {
788
+ guard let publicKey = privateKey?.publicKey.rawRepresentation,
789
+ let signingPublicKey = signingKey?.publicKey.rawRepresentation else { return }
790
+
791
+ // TLV-encoded announcement
792
+ var payload = Data()
793
+
794
+ // Nickname TLV (tag 0x01)
795
+ let nicknameData = Data(myNickname.utf8)
796
+ payload.append(0x01)
797
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(nicknameData.count).bigEndian) { Array($0) })
798
+ payload.append(nicknameData)
799
+
800
+ // Noise public key TLV (tag 0x02)
801
+ payload.append(0x02)
802
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(publicKey.count).bigEndian) { Array($0) })
803
+ payload.append(publicKey)
804
+
805
+ // Signing public key TLV (tag 0x03)
806
+ payload.append(0x03)
807
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(signingPublicKey.count).bigEndian) { Array($0) })
808
+ payload.append(signingPublicKey)
809
+
810
+ let packet = createPacket(type: MessageType.announce.rawValue, payload: payload, recipientID: nil)
811
+ broadcastPacket(packet)
812
+ }
813
+
814
+ private func sendLeaveAnnouncement() {
815
+ let packet = createPacket(type: MessageType.leave.rawValue, payload: Data(), recipientID: nil)
816
+ broadcastPacket(packet)
817
+ }
818
+
819
+ private func initiateHandshakeInternal(with peerId: String) {
820
+ guard let publicKey = privateKey?.publicKey.rawRepresentation else { return }
821
+
822
+ let packet = createPacket(
823
+ type: MessageType.noiseHandshake.rawValue,
824
+ payload: publicKey,
825
+ recipientID: Data(hexString: peerId)
826
+ )
827
+ broadcastPacket(packet)
828
+ }
829
+
830
+ private func createPacket(type: UInt8, payload: Data, recipientID: Data?) -> BitchatPacket {
831
+ var packet = BitchatPacket(
832
+ version: 1,
833
+ type: type,
834
+ senderID: myPeerIDData,
835
+ recipientID: recipientID,
836
+ timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
837
+ payload: payload,
838
+ signature: nil,
839
+ ttl: messageTTL
840
+ )
841
+
842
+ // Sign the packet
843
+ if let sk = signingKey {
844
+ let dataToSign = packet.dataForSigning()
845
+ if let signature = try? sk.signature(for: dataToSign) {
846
+ packet.signature = signature
847
+ }
848
+ }
849
+
850
+ return packet
851
+ }
852
+
853
+ private func broadcastPacket(_ packet: BitchatPacket) {
854
+ guard let data = packet.encode() else { return }
855
+
856
+ // Send to all connected peripherals
857
+ for (_, peripheral) in peripherals {
858
+ if let services = peripheral.services,
859
+ let service = services.first(where: { $0.uuid == BleMesh.serviceUUID }),
860
+ let char = service.characteristics?.first(where: { $0.uuid == BleMesh.characteristicUUID }) {
861
+ peripheral.writeValue(data, for: char, type: .withoutResponse)
862
+ }
863
+ }
864
+
865
+ // Notify all subscribed centrals
866
+ if let char = characteristic {
867
+ peripheralManager?.updateValue(data, for: char, onSubscribedCentrals: nil)
868
+ }
869
+ }
870
+
871
+ private func handleReceivedPacket(_ data: Data, from peripheralUUID: String?) {
872
+ guard let packet = BitchatPacket.decode(from: data) else { return }
873
+
874
+ let senderID = packet.senderID.map { String(format: "%02x", $0) }.joined()
875
+
876
+ // Skip our own packets
877
+ if senderID == myPeerID { return }
878
+
879
+ // Deduplication
880
+ let messageID = "\(senderID)-\(packet.timestamp)-\(packet.type)"
881
+ if processedMessages.contains(messageID) { return }
882
+ processedMessages.insert(messageID)
883
+
884
+ // Handle by type
885
+ switch MessageType(rawValue: packet.type) {
886
+ case .announce:
887
+ handleAnnounce(packet, from: senderID)
888
+ case .message:
889
+ handleMessage(packet, from: senderID)
890
+ case .noiseHandshake:
891
+ handleNoiseHandshake(packet, from: senderID)
892
+ case .noiseEncrypted:
893
+ handleNoiseEncrypted(packet, from: senderID)
894
+ case .leave:
895
+ handleLeave(from: senderID)
896
+ case .fileTransfer:
897
+ handleFileTransfer(packet, from: senderID)
898
+ case .fragment:
899
+ handleFragment(packet, from: senderID)
900
+ case .solanaTransaction:
901
+ handleTransactionMetadata(packet, from: senderID)
902
+ default:
903
+ break
904
+ }
905
+
906
+ // Relay if TTL > 0
907
+ if packet.ttl > 0 {
908
+ var relayPacket = packet
909
+ relayPacket.ttl -= 1
910
+ if let data = relayPacket.encode() {
911
+ bleQueue.asyncAfter(deadline: .now() + Double.random(in: 0.01...0.1)) { [weak self] in
912
+ // Relay to all except source
913
+ self?.relayPacket(data, excluding: peripheralUUID)
914
+ }
915
+ }
916
+ }
917
+ }
918
+
919
+ private func relayPacket(_ data: Data, excluding peripheralUUID: String?) {
920
+ for (uuid, peripheral) in peripherals where uuid != peripheralUUID {
921
+ if let services = peripheral.services,
922
+ let service = services.first(where: { $0.uuid == BleMesh.serviceUUID }),
923
+ let char = service.characteristics?.first(where: { $0.uuid == BleMesh.characteristicUUID }) {
924
+ peripheral.writeValue(data, for: char, type: .withoutResponse)
925
+ }
926
+ }
927
+
928
+ if let char = characteristic {
929
+ peripheralManager?.updateValue(data, for: char, onSubscribedCentrals: nil)
930
+ }
931
+ }
932
+
933
+ private func handleAnnounce(_ packet: BitchatPacket, from senderID: String) {
934
+ // Parse TLV payload
935
+ var nickname = senderID
936
+ var noisePublicKey: Data?
937
+ var signingPublicKey: Data?
938
+
939
+ var offset = 0
940
+ while offset < packet.payload.count {
941
+ guard offset + 3 <= packet.payload.count else { break }
942
+
943
+ let tag = packet.payload[offset]
944
+ let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
945
+ offset += 3
946
+
947
+ guard offset + length <= packet.payload.count else { break }
948
+ let value = packet.payload.subdata(in: offset..<(offset + length))
949
+ offset += length
950
+
951
+ switch tag {
952
+ case 0x01:
953
+ nickname = String(data: value, encoding: .utf8) ?? senderID
954
+ case 0x02:
955
+ noisePublicKey = value
956
+ case 0x03:
957
+ signingPublicKey = value
958
+ default:
959
+ break
960
+ }
961
+ }
962
+
963
+ // Update peer info
964
+ peers[senderID] = PeerInfo(
965
+ peerId: senderID,
966
+ nickname: nickname,
967
+ isConnected: true,
968
+ rssi: nil,
969
+ lastSeen: Date(),
970
+ noisePublicKey: noisePublicKey,
971
+ isVerified: false
972
+ )
973
+
974
+ notifyPeerListUpdated()
975
+ }
976
+
977
+ private func handleMessage(_ packet: BitchatPacket, from senderID: String) {
978
+ guard let content = String(data: packet.payload, encoding: .utf8) else { return }
979
+
980
+ let nickname = peers[senderID]?.nickname ?? senderID
981
+
982
+ let message: [String: Any] = [
983
+ "id": UUID().uuidString,
984
+ "content": content,
985
+ "senderPeerId": senderID,
986
+ "senderNickname": nickname,
987
+ "timestamp": Int(packet.timestamp),
988
+ "isPrivate": false
989
+ ]
990
+
991
+ sendEvent(withName: "onMessageReceived", body: ["message": message])
992
+ }
993
+
994
+ private func handleNoiseHandshake(_ packet: BitchatPacket, from senderID: String) {
995
+ // Store peer's public key and derive shared secret
996
+ guard packet.payload.count == 32,
997
+ let peerPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: packet.payload),
998
+ let pk = privateKey else { return }
999
+
1000
+ do {
1001
+ let sharedSecret = try pk.sharedSecretFromKeyAgreement(with: peerPublicKey)
1002
+ let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
1003
+ using: SHA256.self,
1004
+ salt: Data(),
1005
+ sharedInfo: Data("mesh-encryption".utf8),
1006
+ outputByteCount: 32
1007
+ )
1008
+ sessions[senderID] = symmetricKey.withUnsafeBytes { Data($0) }
1009
+
1010
+ // Update peer's noise public key
1011
+ if var peer = peers[senderID] {
1012
+ peer.noisePublicKey = packet.payload
1013
+ peers[senderID] = peer
1014
+ }
1015
+
1016
+ // Send response handshake if this is an incoming request
1017
+ if packet.recipientID == nil || packet.recipientID == myPeerIDData {
1018
+ initiateHandshakeInternal(with: senderID)
1019
+ }
1020
+ } catch {
1021
+ sendEvent(withName: "onError", body: ["code": "HANDSHAKE_ERROR", "message": error.localizedDescription])
1022
+ }
1023
+ }
1024
+
1025
+ private func handleNoiseEncrypted(_ packet: BitchatPacket, from senderID: String) {
1026
+ // Check if message is for us
1027
+ if let recipientID = packet.recipientID,
1028
+ recipientID != myPeerIDData {
1029
+ return
1030
+ }
1031
+
1032
+ guard let sessionKey = sessions[senderID] else { return }
1033
+
1034
+ // Decrypt
1035
+ guard let decrypted = decryptPayload(packet.payload, with: sessionKey) else { return }
1036
+
1037
+ // Parse payload type
1038
+ guard decrypted.count > 0 else { return }
1039
+ let payloadType = decrypted[0]
1040
+ let payloadData = decrypted.dropFirst()
1041
+
1042
+ switch NoisePayloadType(rawValue: payloadType) {
1043
+ case .privateMessage:
1044
+ handlePrivateMessage(Data(payloadData), from: senderID)
1045
+ case .readReceipt:
1046
+ if let messageId = String(data: payloadData, encoding: .utf8) {
1047
+ sendEvent(withName: "onReadReceipt", body: ["messageId": messageId, "fromPeerId": senderID])
1048
+ }
1049
+ case .deliveryAck:
1050
+ if let messageId = String(data: payloadData, encoding: .utf8) {
1051
+ sendEvent(withName: "onDeliveryAck", body: ["messageId": messageId, "fromPeerId": senderID])
1052
+ }
1053
+ case .solanaTransaction:
1054
+ handleSolanaTransaction(Data(payloadData), from: senderID)
1055
+ case .transactionResponse:
1056
+ handleTransactionResponse(Data(payloadData), from: senderID)
1057
+ default:
1058
+ break
1059
+ }
1060
+ }
1061
+
1062
+ private func handlePrivateMessage(_ data: Data, from senderID: String) {
1063
+ // Parse TLV private message
1064
+ var messageId: String?
1065
+ var content: String?
1066
+
1067
+ var offset = 0
1068
+ while offset < data.count {
1069
+ guard offset + 3 <= data.count else { break }
1070
+ let tag = data[offset]
1071
+ let length = Int(data[offset + 1]) << 8 | Int(data[offset + 2])
1072
+ offset += 3
1073
+
1074
+ guard offset + length <= data.count else { break }
1075
+ let value = data.subdata(in: offset..<(offset + length))
1076
+ offset += length
1077
+
1078
+ switch tag {
1079
+ case 0x01:
1080
+ messageId = String(data: value, encoding: .utf8)
1081
+ case 0x02:
1082
+ content = String(data: value, encoding: .utf8)
1083
+ default:
1084
+ break
1085
+ }
1086
+ }
1087
+
1088
+ guard let id = messageId, let text = content else { return }
1089
+ let nickname = peers[senderID]?.nickname ?? senderID
1090
+
1091
+ let message: [String: Any] = [
1092
+ "id": id,
1093
+ "content": text,
1094
+ "senderPeerId": senderID,
1095
+ "senderNickname": nickname,
1096
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000),
1097
+ "isPrivate": true
1098
+ ]
1099
+
1100
+ sendEvent(withName: "onMessageReceived", body: ["message": message])
1101
+ }
1102
+
1103
+ private func handleSolanaTransaction(_ data: Data, from senderID: String) {
1104
+ // Parse TLV Solana transaction
1105
+ var txId: String?
1106
+ var serializedTransaction: String?
1107
+ var firstSignerPublicKey: String?
1108
+ var secondSignerPublicKey: String?
1109
+ var description: String?
1110
+
1111
+ var offset = 0
1112
+ while offset < data.count {
1113
+ guard offset + 3 <= data.count else { break }
1114
+
1115
+ let tag = data[offset]
1116
+ let length = Int(data[offset + 1]) << 8 | Int(data[offset + 2])
1117
+ offset += 3
1118
+
1119
+ guard offset + length <= data.count else { break }
1120
+ let value = data.subdata(in: offset..<(offset + length))
1121
+ offset += length
1122
+
1123
+ switch tag {
1124
+ case 0x01:
1125
+ txId = String(data: value, encoding: .utf8)
1126
+ case 0x02:
1127
+ serializedTransaction = String(data: value, encoding: .utf8)
1128
+ case 0x03:
1129
+ firstSignerPublicKey = String(data: value, encoding: .utf8)
1130
+ case 0x04:
1131
+ secondSignerPublicKey = String(data: value, encoding: .utf8)
1132
+ case 0x05:
1133
+ description = String(data: value, encoding: .utf8)
1134
+ default:
1135
+ break
1136
+ }
1137
+ }
1138
+
1139
+ guard let id = txId, let tx = serializedTransaction, let firstSigner = firstSignerPublicKey else {
1140
+ print("Invalid Solana transaction data")
1141
+ return
1142
+ }
1143
+
1144
+ var txDict: [String: Any] = [
1145
+ "id": id,
1146
+ "serializedTransaction": tx,
1147
+ "senderPeerId": senderID,
1148
+ "firstSignerPublicKey": firstSigner,
1149
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000),
1150
+ "requiresSecondSigner": true,
1151
+ "openForAnySigner": secondSignerPublicKey == nil
1152
+ ]
1153
+
1154
+ // Second signer is optional - any peer can sign
1155
+ if let secondSigner = secondSignerPublicKey {
1156
+ txDict["secondSignerPublicKey"] = secondSigner
1157
+ }
1158
+
1159
+ if let desc = description {
1160
+ txDict["description"] = desc
1161
+ }
1162
+
1163
+ sendEvent(withName: "onTransactionReceived", body: ["transaction": txDict])
1164
+ print("Received Solana transaction \(id) from \(senderID) (openForAnySigner=\(secondSignerPublicKey == nil))")
1165
+ }
1166
+
1167
+ private func handleTransactionResponse(_ data: Data, from senderID: String) {
1168
+ // Parse TLV transaction response
1169
+ var txId: String?
1170
+ var signedTransaction: String?
1171
+ var error: String?
1172
+
1173
+ var offset = 0
1174
+ while offset < data.count {
1175
+ guard offset + 3 <= data.count else { break }
1176
+
1177
+ let tag = data[offset]
1178
+ let length = Int(data[offset + 1]) << 8 | Int(data[offset + 2])
1179
+ offset += 3
1180
+
1181
+ guard offset + length <= data.count else { break }
1182
+ let value = data.subdata(in: offset..<(offset + length))
1183
+ offset += length
1184
+
1185
+ switch tag {
1186
+ case 0x01:
1187
+ txId = String(data: value, encoding: .utf8)
1188
+ case 0x02:
1189
+ signedTransaction = String(data: value, encoding: .utf8)
1190
+ case 0x03:
1191
+ error = String(data: value, encoding: .utf8)
1192
+ default:
1193
+ break
1194
+ }
1195
+ }
1196
+
1197
+ guard let id = txId else {
1198
+ print("Invalid transaction response")
1199
+ return
1200
+ }
1201
+
1202
+ var responseDict: [String: Any] = [
1203
+ "id": id,
1204
+ "responderPeerId": senderID,
1205
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000)
1206
+ ]
1207
+
1208
+ if let signedTx = signedTransaction {
1209
+ responseDict["signedTransaction"] = signedTx
1210
+ }
1211
+ if let err = error {
1212
+ responseDict["error"] = err
1213
+ }
1214
+
1215
+ sendEvent(withName: "onTransactionResponse", body: ["response": responseDict])
1216
+ print("Received transaction response for \(id) from \(senderID)")
1217
+ }
1218
+
1219
+ private func handleLeave(from senderID: String) {
1220
+ peers.removeValue(forKey: senderID)
1221
+ sessions.removeValue(forKey: senderID)
1222
+ notifyPeerListUpdated()
1223
+ }
1224
+
1225
+ private func encryptMessage(_ content: String, for peerId: String) -> Data? {
1226
+ // Create private message TLV
1227
+ let messageId = UUID().uuidString
1228
+ var tlvData = Data()
1229
+
1230
+ // Message ID TLV (tag 0x01)
1231
+ let idData = Data(messageId.utf8)
1232
+ tlvData.append(0x01)
1233
+ tlvData.append(contentsOf: withUnsafeBytes(of: UInt16(idData.count).bigEndian) { Array($0) })
1234
+ tlvData.append(idData)
1235
+
1236
+ // Content TLV (tag 0x02)
1237
+ let contentData = Data(content.utf8)
1238
+ tlvData.append(0x02)
1239
+ tlvData.append(contentsOf: withUnsafeBytes(of: UInt16(contentData.count).bigEndian) { Array($0) })
1240
+ tlvData.append(contentData)
1241
+
1242
+ // Add payload type prefix
1243
+ var payload = Data([NoisePayloadType.privateMessage.rawValue])
1244
+ payload.append(tlvData)
1245
+
1246
+ return encryptPayload(payload, for: peerId)
1247
+ }
1248
+
1249
+ private func encryptPayload(_ payload: Data, for peerId: String) -> Data? {
1250
+ guard let sessionKey = sessions[peerId] else { return nil }
1251
+
1252
+ do {
1253
+ let key = SymmetricKey(data: sessionKey)
1254
+ let nonce = AES.GCM.Nonce()
1255
+ let sealedBox = try AES.GCM.seal(payload, using: key, nonce: nonce)
1256
+ return sealedBox.combined
1257
+ } catch {
1258
+ return nil
1259
+ }
1260
+ }
1261
+
1262
+ private func decryptPayload(_ encrypted: Data, with sessionKey: Data) -> Data? {
1263
+ do {
1264
+ let key = SymmetricKey(data: sessionKey)
1265
+ let sealedBox = try AES.GCM.SealedBox(combined: encrypted)
1266
+ return try AES.GCM.open(sealedBox, using: key)
1267
+ } catch {
1268
+ return nil
1269
+ }
1270
+ }
1271
+
1272
+ private func notifyPeerListUpdated() {
1273
+ let peerList = peers.values.map { peer in
1274
+ return [
1275
+ "peerId": peer.peerId,
1276
+ "nickname": peer.nickname,
1277
+ "isConnected": peer.isConnected,
1278
+ "rssi": peer.rssi ?? NSNull(),
1279
+ "lastSeen": Int(peer.lastSeen.timeIntervalSince1970 * 1000),
1280
+ "isVerified": peer.isVerified
1281
+ ] as [String: Any]
1282
+ }
1283
+
1284
+ sendEvent(withName: "onPeerListUpdated", body: ["peers": peerList])
1285
+ }
1286
+
1287
+ private func getMimeType(for fileName: String) -> String {
1288
+ let ext = (fileName as NSString).pathExtension.lowercased()
1289
+ switch ext {
1290
+ case "jpg", "jpeg": return "image/jpeg"
1291
+ case "png": return "image/png"
1292
+ case "gif": return "image/gif"
1293
+ case "pdf": return "application/pdf"
1294
+ case "txt": return "text/plain"
1295
+ case "mp4": return "video/mp4"
1296
+ case "mp3": return "audio/mpeg"
1297
+ default: return "application/octet-stream"
1298
+ }
1299
+ }
1300
+
1301
+ private func handleFileTransfer(_ packet: BitchatPacket, from senderID: String) {
1302
+ // Parse file transfer metadata
1303
+ var transferId: String?
1304
+ var fileName: String?
1305
+ var fileSize: Int?
1306
+ var mimeType: String?
1307
+ var totalChunks: Int?
1308
+
1309
+ var offset = 0
1310
+ while offset < packet.payload.count {
1311
+ guard offset + 3 <= packet.payload.count else { break }
1312
+
1313
+ let tag = packet.payload[offset]
1314
+ let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
1315
+ offset += 3
1316
+
1317
+ guard offset + length <= packet.payload.count else { break }
1318
+ let value = packet.payload.subdata(in: offset..<(offset + length))
1319
+ offset += length
1320
+
1321
+ switch tag {
1322
+ case 0x01:
1323
+ transferId = String(data: value, encoding: .utf8)
1324
+ case 0x02:
1325
+ fileName = String(data: value, encoding: .utf8)
1326
+ case 0x03 where value.count == 4:
1327
+ fileSize = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
1328
+ case 0x04:
1329
+ mimeType = String(data: value, encoding: .utf8)
1330
+ case 0x05 where value.count == 4:
1331
+ totalChunks = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
1332
+ default:
1333
+ break
1334
+ }
1335
+ }
1336
+
1337
+ guard let id = transferId, let name = fileName, let size = fileSize, let mime = mimeType, let chunks = totalChunks else {
1338
+ print("Invalid file transfer metadata")
1339
+ return
1340
+ }
1341
+
1342
+ // Store transfer info
1343
+ activeFileTransfers[id] = FileTransfer(
1344
+ id: id,
1345
+ fileName: name,
1346
+ fileSize: size,
1347
+ mimeType: mime,
1348
+ senderPeerId: senderID,
1349
+ totalChunks: chunks,
1350
+ receivedChunks: []
1351
+ )
1352
+ fileTransferFragments[id] = [:]
1353
+
1354
+ print("Started receiving file: \(name) (\(size) bytes, \(chunks) chunks)")
1355
+ }
1356
+
1357
+ private func handleFragment(_ packet: BitchatPacket, from senderID: String) {
1358
+ // Parse fragment - could be file or transaction chunk
1359
+ var id: String?
1360
+ var chunkIndex: Int?
1361
+ var totalChunks: Int?
1362
+ var chunkData: Data?
1363
+
1364
+ var offset = 0
1365
+ while offset < packet.payload.count {
1366
+ guard offset + 3 <= packet.payload.count else { break }
1367
+
1368
+ let tag = packet.payload[offset]
1369
+ let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
1370
+ offset += 3
1371
+
1372
+ guard offset + length <= packet.payload.count else { break }
1373
+ let value = packet.payload.subdata(in: offset..<(offset + length))
1374
+ offset += length
1375
+
1376
+ switch tag {
1377
+ case 0x01:
1378
+ id = String(data: value, encoding: .utf8)
1379
+ case 0x02 where value.count == 4:
1380
+ chunkIndex = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
1381
+ case 0x03 where value.count == 4:
1382
+ totalChunks = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
1383
+ case 0x04:
1384
+ chunkData = value
1385
+ default:
1386
+ break
1387
+ }
1388
+ }
1389
+
1390
+ guard let fragmentId = id, let index = chunkIndex, let chunks = totalChunks, let data = chunkData else {
1391
+ print("Invalid fragment")
1392
+ return
1393
+ }
1394
+
1395
+ // Check if this is a file fragment or transaction chunk
1396
+ if activeFileTransfers[fragmentId] != nil {
1397
+ handleFileFragment(id: fragmentId, chunkIndex: index, totalChunks: chunks, chunkData: data)
1398
+ } else if pendingTransactionChunks[fragmentId] != nil {
1399
+ handleTransactionChunk(id: fragmentId, chunkIndex: index, totalChunks: chunks, chunkData: data)
1400
+ } else {
1401
+ print("Received fragment for unknown transfer/transaction: \(fragmentId)")
1402
+ }
1403
+ }
1404
+
1405
+ private func handleFileFragment(id: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) {
1406
+ guard var transfer = activeFileTransfers[id] else { return }
1407
+
1408
+ // Store the fragment
1409
+ fileTransferFragments[id]?[chunkIndex] = chunkData
1410
+ transfer.receivedChunks.insert(chunkIndex)
1411
+ activeFileTransfers[id] = transfer
1412
+
1413
+ print("Received file chunk \(chunkIndex)/\(totalChunks) for transfer \(id)")
1414
+
1415
+ // Check if we have all chunks
1416
+ if transfer.receivedChunks.count == transfer.totalChunks {
1417
+ // Reassemble file
1418
+ var reassembledData = Data()
1419
+ for i in 0..<transfer.totalChunks {
1420
+ if let chunk = fileTransferFragments[id]?[i] {
1421
+ reassembledData.append(chunk)
1422
+ }
1423
+ }
1424
+
1425
+ // Convert to base64
1426
+ let base64Data = reassembledData.base64EncodedString()
1427
+
1428
+ // Emit file received event
1429
+ let fileDict: [String: Any] = [
1430
+ "id": transfer.id,
1431
+ "fileName": transfer.fileName,
1432
+ "fileSize": transfer.fileSize,
1433
+ "mimeType": transfer.mimeType,
1434
+ "data": base64Data,
1435
+ "senderPeerId": transfer.senderPeerId,
1436
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000)
1437
+ ]
1438
+
1439
+ sendEvent(withName: "onFileReceived", body: ["file": fileDict])
1440
+
1441
+ // Clean up
1442
+ activeFileTransfers.removeValue(forKey: id)
1443
+ fileTransferFragments.removeValue(forKey: id)
1444
+
1445
+ print("File transfer complete: \(transfer.fileName)")
1446
+ }
1447
+ }
1448
+
1449
+ private func handleTransactionChunk(id: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) {
1450
+ guard var pendingTx = pendingTransactionChunks[id] else { return }
1451
+
1452
+ // Store the chunk
1453
+ pendingTx.chunks[chunkIndex] = chunkData
1454
+ pendingTransactionChunks[id] = pendingTx
1455
+
1456
+ print("Received transaction chunk \(chunkIndex)/\(totalChunks) for tx \(id)")
1457
+
1458
+ // Check if we have all chunks
1459
+ if pendingTx.chunks.count == pendingTx.totalChunks {
1460
+ // Reassemble encrypted data
1461
+ var reassembledData = Data()
1462
+ for i in 0..<pendingTx.totalChunks {
1463
+ if let chunk = pendingTx.chunks[i] {
1464
+ reassembledData.append(chunk)
1465
+ }
1466
+ }
1467
+
1468
+ // Decrypt and process
1469
+ if let sessionKey = sessions[pendingTx.senderId] {
1470
+ if let decrypted = decryptPayload(reassembledData, with: sessionKey),
1471
+ decrypted.count > 0 {
1472
+ // Skip the payload type byte and process as Solana transaction
1473
+ let payloadData = decrypted.subdata(in: 1..<decrypted.count)
1474
+ handleSolanaTransaction(payloadData, from: pendingTx.senderId)
1475
+ }
1476
+ }
1477
+
1478
+ // Clean up
1479
+ pendingTransactionChunks.removeValue(forKey: id)
1480
+
1481
+ print("Transaction receive complete: \(id)")
1482
+ }
1483
+ }
1484
+
1485
+ private func handleTransactionMetadata(_ packet: BitchatPacket, from senderID: String) {
1486
+ // Parse transaction chunk metadata
1487
+ var txId: String?
1488
+ var totalSize: Int?
1489
+ var totalChunks: Int?
1490
+
1491
+ var offset = 0
1492
+ while offset < packet.payload.count {
1493
+ guard offset + 3 <= packet.payload.count else { break }
1494
+
1495
+ let tag = packet.payload[offset]
1496
+ let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
1497
+ offset += 3
1498
+
1499
+ guard offset + length <= packet.payload.count else { break }
1500
+ let value = packet.payload.subdata(in: offset..<(offset + length))
1501
+ offset += length
1502
+
1503
+ switch tag {
1504
+ case 0x01:
1505
+ txId = String(data: value, encoding: .utf8)
1506
+ case 0x02 where value.count == 4:
1507
+ totalSize = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
1508
+ case 0x03 where value.count == 4:
1509
+ totalChunks = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
1510
+ default:
1511
+ break
1512
+ }
1513
+ }
1514
+
1515
+ guard let id = txId, let size = totalSize, let chunks = totalChunks else {
1516
+ print("Invalid transaction metadata")
1517
+ return
1518
+ }
1519
+
1520
+ // Store pending transaction chunks info
1521
+ pendingTransactionChunks[id] = TransactionChunks(
1522
+ txId: id,
1523
+ senderId: senderID,
1524
+ totalSize: size,
1525
+ totalChunks: chunks
1526
+ )
1527
+
1528
+ print("Started receiving chunked transaction: \(id) (\(size) bytes, \(chunks) chunks)")
1529
+ }
1530
+ }
1531
+
1532
+ // MARK: - CBCentralManagerDelegate
1533
+
1534
+ extension BleMesh: CBCentralManagerDelegate {
1535
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
1536
+ if central.state == .poweredOn && isRunning {
1537
+ central.scanForPeripherals(
1538
+ withServices: [BleMesh.serviceUUID],
1539
+ options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
1540
+ )
1541
+ }
1542
+
1543
+ let state: String
1544
+ switch central.state {
1545
+ case .poweredOn: state = "connected"
1546
+ case .poweredOff, .unauthorized: state = "disconnected"
1547
+ default: state = "connecting"
1548
+ }
1549
+
1550
+ sendEvent(withName: "onConnectionStateChanged", body: [
1551
+ "state": state,
1552
+ "peerCount": peers.count
1553
+ ])
1554
+ }
1555
+
1556
+ func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
1557
+ let uuid = peripheral.identifier.uuidString
1558
+
1559
+ if peripherals[uuid] == nil {
1560
+ peripherals[uuid] = peripheral
1561
+ peripheral.delegate = self
1562
+ central.connect(peripheral, options: nil)
1563
+ }
1564
+ }
1565
+
1566
+ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
1567
+ peripheral.discoverServices([BleMesh.serviceUUID])
1568
+ }
1569
+
1570
+ func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
1571
+ let uuid = peripheral.identifier.uuidString
1572
+
1573
+ if let peerId = peripheralToPeer[uuid] {
1574
+ if var peer = peers[peerId] {
1575
+ peer.isConnected = false
1576
+ peers[peerId] = peer
1577
+ }
1578
+ notifyPeerListUpdated()
1579
+ }
1580
+
1581
+ // Reconnect
1582
+ if isRunning {
1583
+ central.connect(peripheral, options: nil)
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+ // MARK: - CBPeripheralDelegate
1589
+
1590
+ extension BleMesh: CBPeripheralDelegate {
1591
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
1592
+ guard let services = peripheral.services else { return }
1593
+
1594
+ for service in services where service.uuid == BleMesh.serviceUUID {
1595
+ peripheral.discoverCharacteristics([BleMesh.characteristicUUID], for: service)
1596
+ }
1597
+ }
1598
+
1599
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
1600
+ guard let characteristics = service.characteristics else { return }
1601
+
1602
+ for char in characteristics where char.uuid == BleMesh.characteristicUUID {
1603
+ peripheral.setNotifyValue(true, for: char)
1604
+
1605
+ // Send announce to new peer
1606
+ sendAnnounce()
1607
+ }
1608
+ }
1609
+
1610
+ func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
1611
+ guard let data = characteristic.value else { return }
1612
+ handleReceivedPacket(data, from: peripheral.identifier.uuidString)
1613
+ }
1614
+ }
1615
+
1616
+ // MARK: - CBPeripheralManagerDelegate
1617
+
1618
+ extension BleMesh: CBPeripheralManagerDelegate {
1619
+ func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
1620
+ if peripheral.state == .poweredOn && isRunning {
1621
+ setupPeripheralService()
1622
+ }
1623
+ }
1624
+
1625
+ private func setupPeripheralService() {
1626
+ let char = CBMutableCharacteristic(
1627
+ type: BleMesh.characteristicUUID,
1628
+ properties: [.read, .write, .writeWithoutResponse, .notify],
1629
+ value: nil,
1630
+ permissions: [.readable, .writeable]
1631
+ )
1632
+
1633
+ let service = CBMutableService(type: BleMesh.serviceUUID, primary: true)
1634
+ service.characteristics = [char]
1635
+
1636
+ peripheralManager?.add(service)
1637
+ characteristic = char
1638
+
1639
+ peripheralManager?.startAdvertising([
1640
+ CBAdvertisementDataServiceUUIDsKey: [BleMesh.serviceUUID]
1641
+ ])
1642
+ }
1643
+
1644
+ func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
1645
+ subscribedCentrals.append(central)
1646
+ sendAnnounce()
1647
+ }
1648
+
1649
+ func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
1650
+ subscribedCentrals.removeAll { $0.identifier == central.identifier }
1651
+ }
1652
+
1653
+ func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
1654
+ for request in requests {
1655
+ if let data = request.value {
1656
+ handleReceivedPacket(data, from: nil)
1657
+ }
1658
+ peripheral.respond(to: request, withResult: .success)
1659
+ }
1660
+ }
1661
+ }
1662
+
1663
+ // MARK: - Supporting Types
1664
+
1665
+ enum MessageType: UInt8 {
1666
+ case announce = 0x01
1667
+ case message = 0x02
1668
+ case leave = 0x03
1669
+ case noiseHandshake = 0x04
1670
+ case noiseEncrypted = 0x05
1671
+ case fileTransfer = 0x06
1672
+ case fragment = 0x07
1673
+ case requestSync = 0x08
1674
+ case solanaTransaction = 0x09
1675
+ }
1676
+
1677
+ enum NoisePayloadType: UInt8 {
1678
+ case privateMessage = 0x01
1679
+ case readReceipt = 0x02
1680
+ case deliveryAck = 0x03
1681
+ case fileTransfer = 0x04
1682
+ case verifyChallenge = 0x05
1683
+ case verifyResponse = 0x06
1684
+ case solanaTransaction = 0x07
1685
+ case transactionResponse = 0x08
1686
+ }
1687
+
1688
+ struct BitchatPacket {
1689
+ var version: UInt8 = 1
1690
+ var type: UInt8
1691
+ var senderID: Data
1692
+ var recipientID: Data?
1693
+ var timestamp: UInt64
1694
+ var payload: Data
1695
+ var signature: Data?
1696
+ var ttl: UInt8
1697
+
1698
+ func dataForSigning() -> Data {
1699
+ var data = Data()
1700
+ data.append(version)
1701
+ data.append(type)
1702
+ data.append(senderID)
1703
+ if let recipientID = recipientID {
1704
+ data.append(recipientID)
1705
+ }
1706
+ data.append(contentsOf: withUnsafeBytes(of: timestamp.bigEndian) { Array($0) })
1707
+ data.append(payload)
1708
+ data.append(ttl)
1709
+ return data
1710
+ }
1711
+
1712
+ func encode() -> Data? {
1713
+ var data = Data()
1714
+
1715
+ // Version
1716
+ data.append(version)
1717
+
1718
+ // Type
1719
+ data.append(type)
1720
+
1721
+ // TTL
1722
+ data.append(ttl)
1723
+
1724
+ // Sender ID (8 bytes)
1725
+ data.append(senderID.prefix(8))
1726
+ if senderID.count < 8 {
1727
+ data.append(Data(count: 8 - senderID.count))
1728
+ }
1729
+
1730
+ // Recipient ID (8 bytes, or zeros for broadcast)
1731
+ if let recipientID = recipientID {
1732
+ data.append(recipientID.prefix(8))
1733
+ if recipientID.count < 8 {
1734
+ data.append(Data(count: 8 - recipientID.count))
1735
+ }
1736
+ } else {
1737
+ data.append(Data(count: 8))
1738
+ }
1739
+
1740
+ // Timestamp (8 bytes)
1741
+ data.append(contentsOf: withUnsafeBytes(of: timestamp.bigEndian) { Array($0) })
1742
+
1743
+ // Payload length (2 bytes)
1744
+ let payloadLength = UInt16(payload.count)
1745
+ data.append(contentsOf: withUnsafeBytes(of: payloadLength.bigEndian) { Array($0) })
1746
+
1747
+ // Payload
1748
+ data.append(payload)
1749
+
1750
+ // Signature (64 bytes if present)
1751
+ if let signature = signature {
1752
+ data.append(signature)
1753
+ }
1754
+
1755
+ return data
1756
+ }
1757
+
1758
+ static func decode(from data: Data) -> BitchatPacket? {
1759
+ guard data.count >= 29 else { return nil } // Minimum packet size
1760
+
1761
+ var offset = 0
1762
+
1763
+ // Version
1764
+ let version = data[offset]
1765
+ offset += 1
1766
+
1767
+ // Type
1768
+ let type = data[offset]
1769
+ offset += 1
1770
+
1771
+ // TTL
1772
+ let ttl = data[offset]
1773
+ offset += 1
1774
+
1775
+ // Sender ID (8 bytes)
1776
+ let senderID = data.subdata(in: offset..<(offset + 8))
1777
+ offset += 8
1778
+
1779
+ // Recipient ID (8 bytes)
1780
+ let recipientIDData = data.subdata(in: offset..<(offset + 8))
1781
+ let recipientID = recipientIDData.allSatisfy({ $0 == 0 }) ? nil : recipientIDData
1782
+ offset += 8
1783
+
1784
+ // Timestamp (8 bytes)
1785
+ let timestampData = data.subdata(in: offset..<(offset + 8))
1786
+ let timestamp = timestampData.withUnsafeBytes { $0.load(as: UInt64.self).bigEndian }
1787
+ offset += 8
1788
+
1789
+ // Payload length (2 bytes)
1790
+ let lengthData = data.subdata(in: offset..<(offset + 2))
1791
+ let payloadLength = Int(lengthData.withUnsafeBytes { $0.load(as: UInt16.self).bigEndian })
1792
+ offset += 2
1793
+
1794
+ guard offset + payloadLength <= data.count else { return nil }
1795
+
1796
+ // Payload
1797
+ let payload = data.subdata(in: offset..<(offset + payloadLength))
1798
+ offset += payloadLength
1799
+
1800
+ // Signature (64 bytes if present)
1801
+ var signature: Data?
1802
+ if offset + 64 <= data.count {
1803
+ signature = data.subdata(in: offset..<(offset + 64))
1804
+ }
1805
+
1806
+ return BitchatPacket(
1807
+ version: version,
1808
+ type: type,
1809
+ senderID: senderID,
1810
+ recipientID: recipientID,
1811
+ timestamp: timestamp,
1812
+ payload: payload,
1813
+ signature: signature,
1814
+ ttl: ttl
1815
+ )
1816
+ }
1817
+ }
1818
+
1819
+ // MARK: - Data Extension
1820
+
1821
+ extension Data {
1822
+ init?(hexString: String?) {
1823
+ guard let hexString = hexString else { return nil }
1824
+
1825
+ let len = hexString.count / 2
1826
+ var data = Data(capacity: len)
1827
+ var index = hexString.startIndex
1828
+
1829
+ for _ in 0..<len {
1830
+ let nextIndex = hexString.index(index, offsetBy: 2)
1831
+ guard let byte = UInt8(hexString[index..<nextIndex], radix: 16) else { return nil }
1832
+ data.append(byte)
1833
+ index = nextIndex
1834
+ }
1835
+
1836
+ self = data
1837
+ }
1838
+ }