@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.
package/src/index.ts ADDED
@@ -0,0 +1,448 @@
1
+ import { NativeModules, NativeEventEmitter, Platform, PermissionsAndroid, Permission } from 'react-native';
2
+
3
+ const LINKING_ERROR =
4
+ `The package 'kard-network-ble-mesh' doesn't seem to be linked. Make sure: \n\n` +
5
+ Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
6
+ '- You rebuilt the app after installing the package\n' +
7
+ '- You are not using Expo Go (this package requires a development build)\n';
8
+
9
+ const BleMeshModule = NativeModules.BleMesh
10
+ ? NativeModules.BleMesh
11
+ : new Proxy(
12
+ {},
13
+ {
14
+ get() {
15
+ throw new Error(LINKING_ERROR);
16
+ },
17
+ }
18
+ );
19
+
20
+ const eventEmitter = new NativeEventEmitter(BleMeshModule);
21
+
22
+ // Types
23
+ export interface Peer {
24
+ peerId: string;
25
+ nickname: string;
26
+ isConnected: boolean;
27
+ rssi?: number;
28
+ lastSeen: number;
29
+ isVerified: boolean;
30
+ }
31
+
32
+ export interface Message {
33
+ id: string;
34
+ content: string;
35
+ senderPeerId: string;
36
+ senderNickname: string;
37
+ timestamp: number;
38
+ isPrivate: boolean;
39
+ channel?: string;
40
+ }
41
+
42
+ export interface FileTransfer {
43
+ id: string;
44
+ fileName: string;
45
+ fileSize: number;
46
+ mimeType: string;
47
+ data: string; // base64 encoded
48
+ senderPeerId: string;
49
+ timestamp: number;
50
+ }
51
+
52
+ export interface SolanaTransaction {
53
+ id: string;
54
+ /** Base64-encoded serialized transaction (could be partially signed) */
55
+ serializedTransaction: string;
56
+ /** The peer ID who initiated/sent the transaction */
57
+ senderPeerId: string;
58
+ /** The public key of the first signer (sender) */
59
+ firstSignerPublicKey: string;
60
+ /**
61
+ * The preferred public key of the second signer (optional).
62
+ * If not provided, any peer can sign as the second signer.
63
+ */
64
+ secondSignerPublicKey?: string;
65
+ /** Description or purpose of the transaction */
66
+ description?: string;
67
+ /** Timestamp when transaction was sent */
68
+ timestamp: number;
69
+ /** Whether this transaction requires a second signer to sign */
70
+ requiresSecondSigner: boolean;
71
+ }
72
+
73
+ export interface TransactionResponse {
74
+ id: string;
75
+ /** The peer ID who responded */
76
+ responderPeerId: string;
77
+ /** Base64-encoded fully signed transaction (after second signer signs) */
78
+ signedTransaction?: string;
79
+ /** Error message if signing failed */
80
+ error?: string;
81
+ /** Timestamp of response */
82
+ timestamp: number;
83
+ }
84
+
85
+ export interface SolanaNonceTransaction {
86
+ recentBlockhash: string;
87
+ nonceAccount: string;
88
+ nonceAuthority: string;
89
+ feePayer: string;
90
+ instructions: Array<{
91
+ programId: string;
92
+ keys: Array<{ pubkey: string; isSigner: boolean; isWritable: boolean }>;
93
+ data: string; // base64 encoded
94
+ }>;
95
+ }
96
+
97
+ export interface PermissionStatus {
98
+ bluetooth: boolean;
99
+ bluetoothAdvertise?: boolean; // Android 12+
100
+ bluetoothConnect?: boolean; // Android 12+
101
+ bluetoothScan?: boolean; // Android 12+
102
+ location: boolean;
103
+ }
104
+
105
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'scanning';
106
+
107
+ export interface MeshServiceConfig {
108
+ nickname?: string;
109
+ autoRequestPermissions?: boolean;
110
+ }
111
+
112
+ // Event types
113
+ export type PeerListUpdatedEvent = { peers: Peer[] };
114
+ export type MessageReceivedEvent = { message: Message };
115
+ export type FileReceivedEvent = { file: FileTransfer };
116
+ export type TransactionReceivedEvent = { transaction: SolanaTransaction };
117
+ export type TransactionResponseEvent = { response: TransactionResponse };
118
+ export type ConnectionStateChangedEvent = { state: ConnectionState; peerCount: number };
119
+ export type PermissionsChangedEvent = { permissions: PermissionStatus };
120
+ export type ErrorEvent = { code: string; message: string };
121
+
122
+ // Event listener types
123
+ type EventCallback<T> = (data: T) => void;
124
+
125
+ class BleMeshService {
126
+ private isInitialized = false;
127
+
128
+ // Request all required permissions for BLE mesh networking
129
+ async requestPermissions(): Promise<PermissionStatus> {
130
+ if (Platform.OS === 'android') {
131
+ return this.requestAndroidPermissions();
132
+ } else if (Platform.OS === 'ios') {
133
+ return this.requestIOSPermissions();
134
+ }
135
+ throw new Error('Unsupported platform');
136
+ }
137
+
138
+ private async requestAndroidPermissions(): Promise<PermissionStatus> {
139
+ const apiLevel = Platform.Version as number;
140
+
141
+ let permissions: Permission[] = [];
142
+
143
+ if (apiLevel >= 31) {
144
+ // Android 12+ (API 31+)
145
+ permissions = [
146
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE as Permission,
147
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT as Permission,
148
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN as Permission,
149
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION as Permission,
150
+ ];
151
+ } else {
152
+ // Android 11 and below
153
+ permissions = [
154
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION as Permission,
155
+ PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION as Permission,
156
+ ];
157
+ }
158
+
159
+ const results = await PermissionsAndroid.requestMultiple(permissions);
160
+
161
+ const status: PermissionStatus = {
162
+ bluetooth: true,
163
+ location: results[PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION] === 'granted',
164
+ };
165
+
166
+ if (apiLevel >= 31) {
167
+ status.bluetoothAdvertise = results[PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE] === 'granted';
168
+ status.bluetoothConnect = results[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] === 'granted';
169
+ status.bluetoothScan = results[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] === 'granted';
170
+ status.bluetooth = status.bluetoothAdvertise && status.bluetoothConnect && status.bluetoothScan;
171
+ }
172
+
173
+ return status;
174
+ }
175
+
176
+ private async requestIOSPermissions(): Promise<PermissionStatus> {
177
+ // On iOS, Bluetooth permissions are requested automatically when starting services
178
+ // The native module handles the permission prompts
179
+ const result = await BleMeshModule.requestPermissions();
180
+ return {
181
+ bluetooth: result.bluetooth,
182
+ location: result.location,
183
+ };
184
+ }
185
+
186
+ // Check current permission status without requesting
187
+ async checkPermissions(): Promise<PermissionStatus> {
188
+ if (Platform.OS === 'android') {
189
+ return this.checkAndroidPermissions();
190
+ }
191
+ return BleMeshModule.checkPermissions();
192
+ }
193
+
194
+ private async checkAndroidPermissions(): Promise<PermissionStatus> {
195
+ const apiLevel = Platform.Version as number;
196
+
197
+ const fineLocation = await PermissionsAndroid.check(
198
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
199
+ );
200
+
201
+ const status: PermissionStatus = {
202
+ bluetooth: true,
203
+ location: fineLocation,
204
+ };
205
+
206
+ if (apiLevel >= 31) {
207
+ const advertise = await PermissionsAndroid.check(
208
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE
209
+ );
210
+ const connect = await PermissionsAndroid.check(
211
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT
212
+ );
213
+ const scan = await PermissionsAndroid.check(
214
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN
215
+ );
216
+
217
+ status.bluetoothAdvertise = advertise;
218
+ status.bluetoothConnect = connect;
219
+ status.bluetoothScan = scan;
220
+ status.bluetooth = advertise && connect && scan;
221
+ }
222
+
223
+ return status;
224
+ }
225
+
226
+ // Initialize and start the mesh service
227
+ async start(config: MeshServiceConfig = {}): Promise<void> {
228
+ const { nickname = 'anon', autoRequestPermissions = true } = config;
229
+
230
+ if (autoRequestPermissions) {
231
+ const permissions = await this.requestPermissions();
232
+ const hasAllPermissions = permissions.bluetooth && permissions.location;
233
+
234
+ if (!hasAllPermissions) {
235
+ throw new Error('Required permissions not granted. Bluetooth and Location permissions are required.');
236
+ }
237
+ }
238
+
239
+ await BleMeshModule.start(nickname);
240
+ this.isInitialized = true;
241
+ }
242
+
243
+ // Stop the mesh service
244
+ async stop(): Promise<void> {
245
+ if (!this.isInitialized) return;
246
+ await BleMeshModule.stop();
247
+ this.isInitialized = false;
248
+ }
249
+
250
+ // Set the user's nickname
251
+ async setNickname(nickname: string): Promise<void> {
252
+ this.ensureInitialized();
253
+ await BleMeshModule.setNickname(nickname);
254
+ }
255
+
256
+ // Get the current peer ID
257
+ async getMyPeerId(): Promise<string> {
258
+ this.ensureInitialized();
259
+ return BleMeshModule.getMyPeerId();
260
+ }
261
+
262
+ // Get the current nickname
263
+ async getMyNickname(): Promise<string> {
264
+ this.ensureInitialized();
265
+ return BleMeshModule.getMyNickname();
266
+ }
267
+
268
+ // Get list of currently connected peers
269
+ async getPeers(): Promise<Peer[]> {
270
+ this.ensureInitialized();
271
+ return BleMeshModule.getPeers();
272
+ }
273
+
274
+ // Send a public broadcast message to all peers
275
+ async sendMessage(content: string, channel?: string): Promise<string> {
276
+ this.ensureInitialized();
277
+ return BleMeshModule.sendMessage(content, channel || null);
278
+ }
279
+
280
+ // Send a private encrypted message to a specific peer
281
+ async sendPrivateMessage(content: string, recipientPeerId: string): Promise<string> {
282
+ this.ensureInitialized();
283
+ return BleMeshModule.sendPrivateMessage(content, recipientPeerId);
284
+ }
285
+
286
+ // Send a file (broadcast or private)
287
+ async sendFile(
288
+ filePath: string,
289
+ options?: { recipientPeerId?: string; channel?: string }
290
+ ): Promise<string> {
291
+ this.ensureInitialized();
292
+ return BleMeshModule.sendFile(filePath, options?.recipientPeerId || null, options?.channel || null);
293
+ }
294
+
295
+ /**
296
+ * Send a Solana transaction for any peer to sign as second signer.
297
+ *
298
+ * @param serializedTransaction - Base64-encoded partially signed transaction
299
+ * @param options - Transaction options
300
+ * @param options.firstSignerPublicKey - Public key of the first signer (required)
301
+ * @param options.secondSignerPublicKey - Preferred second signer (optional, any peer can sign if not specified)
302
+ * @param options.description - Description of the transaction
303
+ * @param options.recipientPeerId - Specific peer to send to (optional, broadcasts to all if not specified)
304
+ */
305
+ async sendTransaction(
306
+ serializedTransaction: string,
307
+ options?: {
308
+ firstSignerPublicKey: string;
309
+ secondSignerPublicKey?: string;
310
+ description?: string;
311
+ recipientPeerId?: string;
312
+ }
313
+ ): Promise<string> {
314
+ this.ensureInitialized();
315
+ const txId = Math.random().toString(36).substring(2, 15);
316
+ return BleMeshModule.sendTransaction(
317
+ txId,
318
+ serializedTransaction,
319
+ options?.recipientPeerId || null,
320
+ options?.firstSignerPublicKey,
321
+ options?.secondSignerPublicKey || null,
322
+ options?.description || null
323
+ );
324
+ }
325
+
326
+ // Respond to a received transaction (sign or reject)
327
+ async respondToTransaction(
328
+ transactionId: string,
329
+ recipientPeerId: string,
330
+ response: {
331
+ signedTransaction?: string;
332
+ error?: string;
333
+ }
334
+ ): Promise<void> {
335
+ this.ensureInitialized();
336
+ await BleMeshModule.respondToTransaction(
337
+ transactionId,
338
+ recipientPeerId,
339
+ response.signedTransaction || null,
340
+ response.error || null
341
+ );
342
+ }
343
+
344
+ // Send a read receipt for a message
345
+ async sendReadReceipt(messageId: string, recipientPeerId: string): Promise<void> {
346
+ this.ensureInitialized();
347
+ await BleMeshModule.sendReadReceipt(messageId, recipientPeerId);
348
+ }
349
+
350
+ // Check if we have an encrypted session with a peer
351
+ async hasEncryptedSession(peerId: string): Promise<boolean> {
352
+ this.ensureInitialized();
353
+ return BleMeshModule.hasEncryptedSession(peerId);
354
+ }
355
+
356
+ // Initiate a Noise handshake with a peer
357
+ async initiateHandshake(peerId: string): Promise<void> {
358
+ this.ensureInitialized();
359
+ await BleMeshModule.initiateHandshake(peerId);
360
+ }
361
+
362
+ async sendSolanaNonceTransaction(
363
+ transaction: SolanaNonceTransaction,
364
+ recipientPeerId: string
365
+ ): Promise<string> {
366
+ this.ensureInitialized();
367
+ return BleMeshModule.sendSolanaNonceTransaction(transaction, recipientPeerId);
368
+ }
369
+
370
+ // Get the identity fingerprint for verification
371
+ async getIdentityFingerprint(): Promise<string> {
372
+ this.ensureInitialized();
373
+ return BleMeshModule.getIdentityFingerprint();
374
+ }
375
+
376
+ // Get peer's fingerprint for verification
377
+ async getPeerFingerprint(peerId: string): Promise<string | null> {
378
+ this.ensureInitialized();
379
+ return BleMeshModule.getPeerFingerprint(peerId);
380
+ }
381
+
382
+ // Force a broadcast announce to refresh presence
383
+ async broadcastAnnounce(): Promise<void> {
384
+ this.ensureInitialized();
385
+ await BleMeshModule.broadcastAnnounce();
386
+ }
387
+
388
+ // Event listeners
389
+ onPeerListUpdated(callback: EventCallback<PeerListUpdatedEvent>): () => void {
390
+ const subscription = eventEmitter.addListener('onPeerListUpdated', callback);
391
+ return () => subscription.remove();
392
+ }
393
+
394
+ onMessageReceived(callback: EventCallback<MessageReceivedEvent>): () => void {
395
+ const subscription = eventEmitter.addListener('onMessageReceived', callback);
396
+ return () => subscription.remove();
397
+ }
398
+
399
+ onFileReceived(callback: EventCallback<FileReceivedEvent>): () => void {
400
+ const subscription = eventEmitter.addListener('onFileReceived', callback);
401
+ return () => subscription.remove();
402
+ }
403
+
404
+ onTransactionReceived(callback: EventCallback<TransactionReceivedEvent>): () => void {
405
+ const subscription = eventEmitter.addListener('onTransactionReceived', callback);
406
+ return () => subscription.remove();
407
+ }
408
+
409
+ onTransactionResponse(callback: EventCallback<TransactionResponseEvent>): () => void {
410
+ const subscription = eventEmitter.addListener('onTransactionResponse', callback);
411
+ return () => subscription.remove();
412
+ }
413
+
414
+ onConnectionStateChanged(callback: EventCallback<ConnectionStateChangedEvent>): () => void {
415
+ const subscription = eventEmitter.addListener('onConnectionStateChanged', callback);
416
+ return () => subscription.remove();
417
+ }
418
+
419
+ onReadReceipt(callback: EventCallback<{ messageId: string; fromPeerId: string }>): () => void {
420
+ const subscription = eventEmitter.addListener('onReadReceipt', callback);
421
+ return () => subscription.remove();
422
+ }
423
+
424
+ onDeliveryAck(callback: EventCallback<{ messageId: string; fromPeerId: string }>): () => void {
425
+ const subscription = eventEmitter.addListener('onDeliveryAck', callback);
426
+ return () => subscription.remove();
427
+ }
428
+
429
+ onError(callback: EventCallback<ErrorEvent>): () => void {
430
+ const subscription = eventEmitter.addListener('onError', callback);
431
+ return () => subscription.remove();
432
+ }
433
+
434
+ private ensureInitialized(): void {
435
+ if (!this.isInitialized) {
436
+ throw new Error('BleMesh service not initialized. Call start() first.');
437
+ }
438
+ }
439
+ }
440
+
441
+ // Export singleton instance
442
+ export const BleMesh = new BleMeshService();
443
+
444
+ // Also export the class for those who want multiple instances
445
+ export { BleMeshService };
446
+
447
+ // Default export
448
+ export default BleMesh;