BLE Mesh networking library for React Native with seamless permissions
npm install @magicred-1/ble-meshA React Native library for BLE (Bluetooth Low Energy) mesh networking. Build decentralized, peer-to-peer messaging apps that work without internet connectivity.
- Cross-platform: Works on both iOS and Android
- Mesh networking: Messages automatically relay through peers
- End-to-end encryption: Noise protocol for secure private messages
- File transfer: Send files over BLE mesh (chunked for reliability)
- Solana transactions: Multi-signature transaction signing over BLE
- Seamless permissions: Handles permission requests automatically
- Message deduplication: Built-in deduplication prevents duplicate messages
- Peer discovery: Automatic discovery of nearby devices
``bash`
npm install ble-meshor
yarn add ble-mesh
`bash`
cd ios && pod install
Add the following to your Info.plist:
`xml`
Add the following permissions to your AndroidManifest.xml (they are already included in the library manifest):
`xml`
`typescript
import { BleMesh } from 'ble-mesh';
// Start the mesh service (automatically requests permissions)
await BleMesh.start({ nickname: 'Alice' });
// Listen for events
BleMesh.onPeerListUpdated(({ peers }) => {
console.log('Connected peers:', peers);
});
BleMesh.onMessageReceived(({ message }) => {
console.log('Received message:', message);
});
// Send a public broadcast message
await BleMesh.sendMessage('Hello everyone!');
// Send a private encrypted message
await BleMesh.sendPrivateMessage('Secret message', recipientPeerId);
// Get current peers
const peers = await BleMesh.getPeers();
// Stop the service
await BleMesh.stop();
`
The main singleton instance for interacting with the BLE mesh.
#### Methods
##### start(config?: MeshServiceConfig): Promise
Starts the mesh service. Automatically requests required permissions.
`typescript`
interface MeshServiceConfig {
nickname?: string; // Default: 'anon'
autoRequestPermissions?: boolean; // Default: true
}
##### stop(): Promise
Stops the mesh service and disconnects from all peers.
##### requestPermissions(): Promise
Requests all required Bluetooth and location permissions. Called automatically by start() unless autoRequestPermissions: false.
`typescript
interface PermissionStatus {
bluetooth: boolean;
bluetoothAdvertise?: boolean; // Android 12+
bluetoothConnect?: boolean; // Android 12+
bluetoothScan?: boolean; // Android 12+
location: boolean;
}
const permissions = await BleMesh.requestPermissions();
if (!permissions.bluetooth || !permissions.location) {
console.error('Required permissions not granted');
}
`
##### checkPermissions(): Promise
Checks current permission status without requesting. Useful for showing permission UI state.
`typescript`
const permissions = await BleMesh.checkPermissions();
console.log('Bluetooth enabled:', permissions.bluetooth);
console.log('Location enabled:', permissions.location);
##### setNickname(nickname: string): Promise
Updates your nickname and broadcasts the change to peers.
##### getMyPeerId(): Promise
Returns your unique peer ID (derived from your public key fingerprint).
##### getMyNickname(): Promise
Returns your current nickname.
##### getPeers(): Promise
Returns an array of all discovered peers.
`typescript`
interface Peer {
peerId: string;
nickname: string;
isConnected: boolean;
rssi?: number;
lastSeen: number;
isVerified: boolean;
}
##### sendMessage(content: string, channel?: string): Promise
Sends a public broadcast message. Returns the message ID.
##### sendPrivateMessage(content: string, recipientPeerId: string): Promise
Sends an encrypted private message to a specific peer. Returns the message ID.
##### sendReadReceipt(messageId: string, recipientPeerId: string): Promise
Sends a read receipt for a received message.
##### sendFile(filePath: string, options?: { recipientPeerId?: string; channel?: string }): Promise
Sends a file over the mesh network. Files are automatically chunked for reliable transmission.
`typescript
// Send to all peers (broadcast)
await BleMesh.sendFile('/path/to/photo.jpg');
// Send to specific peer
await BleMesh.sendFile('/path/to/document.pdf', { recipientPeerId: 'abc123' });
`
##### sendTransaction(serializedTransaction: string, options): Promise
Sends a Solana transaction for any peer to sign as the second signer. Perfect for multi-signature transactions.
> ⚠️ Important: Transactions require encrypted sessions with target peer(s). By default, sendTransaction() automatically initiates handshakes with peers that don't have active sessions. To disable auto-handshake, set ensureSession: false in options.
Targeted (specific peer):
`typescript`
await BleMesh.sendTransaction(base64SerializedTx, {
recipientPeerId: 'abc123', // Send to specific peer (optional)
firstSignerPublicKey: senderPubKey,
secondSignerPublicKey: recipientPubKey, // Preferred signer (optional)
description: 'Payment for services',
ensureSession: true // Default: auto-initiate handshake if needed
});
Broadcast (any peer can sign):
`typescript`
// Broadcast to all peers - any peer can sign
// Automatically initiates handshakes with all connected peers
await BleMesh.sendTransaction(base64SerializedTx, {
firstSignerPublicKey: senderPubKey,
// No recipientPeerId - broadcasts to all
// No secondSignerPublicKey - open for any signer
description: 'Open offer - anyone can complete'
});
Manual session management (advanced):
`typescript
// Check if encrypted session exists
const hasSession = await BleMesh.hasEncryptedSession(peerId);
if (!hasSession) {
// Manually initiate handshake
await BleMesh.initiateHandshake(peerId);
// Wait briefly for handshake to complete
await new Promise(resolve => setTimeout(resolve, 500));
}
// Now send transaction without auto-handshake
await BleMesh.sendTransaction(base64SerializedTx, {
recipientPeerId: peerId,
firstSignerPublicKey: senderPubKey,
ensureSession: false // Skip auto-handshake
});
`
##### respondToTransaction(transactionId: string, recipientPeerId: string, response): Promise
Responds to a received transaction with the signed transaction or an error.
`typescript
// Approve and sign
await BleMesh.respondToTransaction(txId, senderPeerId, {
signedTransaction: fullySignedBase64Tx
});
// Reject
await BleMesh.respondToTransaction(txId, senderPeerId, {
error: 'User rejected transaction'
});
`
##### hasEncryptedSession(peerId: string): Promise
Checks if an encrypted session exists with the specified peer. Useful for verifying secure communication is established before sending sensitive data.
`typescript`
const hasSession = await BleMesh.hasEncryptedSession(peerId);
if (hasSession) {
console.log('Secure session active');
} else {
console.log('No encrypted session - need handshake');
}
##### initiateHandshake(peerId: string): Promise
Initiates a Noise handshake with a peer for encrypted communication. Required before sending private messages or transactions.
`typescript
// Manually establish encrypted session
await BleMesh.initiateHandshake(peerId);
await new Promise(resolve => setTimeout(resolve, 500)); // Wait for completion
// Now send encrypted data
await BleMesh.sendPrivateMessage('Secret data', peerId);
`
##### sendSolanaNonceTransaction(transaction: SolanaNonceTransaction, recipientPeerId: string): Promise
Sends a durable transaction using Solana nonce accounts for offline transaction signing.
`typescript
interface SolanaNonceTransaction {
recentBlockhash: string;
nonceAccount: string;
nonceAuthority: string;
feePayer: string;
instructions: Array<{
programId: string;
keys: Array<{ pubkey: string; isSigner: boolean; isWritable: boolean }>;
data: string; // base64 encoded
}>;
}
const txId = await BleMesh.sendSolanaNonceTransaction({
recentBlockhash: nonceAccountValue,
nonceAccount: nonceAccountPubkey,
nonceAuthority: authorityPubkey,
feePayer: feePayerPubkey,
instructions: [/ ... /]
}, recipientPeerId);
`
##### getIdentityFingerprint(): Promise
Returns your identity fingerprint for verification. Share this with peers to verify your identity.
##### getPeerFingerprint(peerId: string): Promise
Returns a peer's identity fingerprint for verification. Compare with their shared fingerprint to verify identity.
`typescript`
const peerFingerprint = await BleMesh.getPeerFingerprint(peerId);
if (peerFingerprint === sharedFingerprint) {
console.log('Identity verified!');
} else {
console.warn('Identity mismatch - possible MITM attack');
}
##### broadcastAnnounce(): Promise
Forces a broadcast announce to refresh your presence on the mesh. Useful for re-announcing after network connectivity changes.
#### Events
##### onPeerListUpdated(callback): () => void
Called when the peer list changes.
`typescript`
BleMesh.onPeerListUpdated(({ peers }) => {
console.log('Peers updated:', peers);
});
##### onMessageReceived(callback): () => void
Called when a message is received.
`typescript`
BleMesh.onMessageReceived(({ message }) => {
console.log('Message from', message.senderNickname, ':', message.content);
});
##### onConnectionStateChanged(callback): () => void
Called when the connection state changes.
##### onReadReceipt(callback): () => void
Called when a read receipt is received.
##### onDeliveryAck(callback): () => void
Called when a delivery acknowledgment is received.
##### onFileReceived(callback): () => void
Called when a file is received.
`typescript`
BleMesh.onFileReceived(({ file }) => {
console.log('Received file:', file.fileName);
console.log('Size:', file.fileSize);
console.log('Data (base64):', file.data);
// Save or display the file...
});
##### onTransactionReceived(callback): () => void
Called when a Solana transaction is received for signing.
`typescript`
BleMesh.onTransactionReceived(({ transaction }) => {
console.log('Transaction to sign:', transaction.id);
console.log('Serialized:', transaction.serializedTransaction);
console.log('First signer:', transaction.firstSignerPublicKey);
// Check if a specific second signer is required
if (transaction.secondSignerPublicKey) {
console.log('Required second signer:', transaction.secondSignerPublicKey);
// Verify this is your public key before signing
} else if (transaction.openForAnySigner) {
console.log('Any peer can sign this transaction');
}
// Sign with your wallet...
const signedTx = await yourWallet.signTransaction(transaction.serializedTransaction);
// Respond
await BleMesh.respondToTransaction(transaction.id, transaction.senderPeerId, {
signedTransaction: signedTx
});
});
##### onTransactionResponse(callback): () => void
Called when a transaction response is received (signed or rejected).
`typescript`
BleMesh.onTransactionResponse(({ response }) => {
if (response.signedTransaction) {
console.log('Transaction fully signed!');
// Broadcast to Solana network...
} else if (response.error) {
console.log('Transaction rejected:', response.error);
}
});
##### onError(callback): () => void
Called when an error occurs.
``
Alice (First Signer) Bob (Second Signer)
| |
| 1. Create transaction |
| 2. Sign with Alice's key |
| 3. Send via BleMesh.sendTransaction() |
| with recipientPeerId |
|--------------------------------------->|
| |
| 4. onTransactionReceived event |
|<---------------------------------------|
| |
| 5. Review & sign with Bob's key |
|<---------------------------------------|
| |
| 6. respondToTransaction() |
|--------------------------------------->|
| |
| 7. Broadcast fully signed tx |
v to Solana network v
```
Alice (First Signer) Bob Carol Dave
| | | |
| 1. Create tx | | |
| 2. Sign | | |
| 3. Broadcast | | |
|---------------------->|------------>|------------>|
| | | |
| | 4. All receive |
| | onTransactionReceived |
| | | |
| | 5. Carol decides to sign |
| | | |
| |<------------|------------>|
| | 6. Carol responds |
|<----------------------|-------------|-------------|
| | | |
| 7. Alice receives | | |
| signed tx | | |
v v v v
The library automatically handles BLE MTU limitations:
| Feature | Max Size | Handling |
|---------|----------|----------|
| Simple messages | ~450 bytes | Single packet |
| Small transactions | ~450 bytes | Single packet |
| Large transactions | Unlimited | Automatic chunking (400 byte chunks) |
| Files | Unlimited | Chunked (180 byte chunks) |
For Solana transactions:
- Most transactions (simple transfers) fit in a single packet
- Complex transactions (multi-instruction, large memos) are automatically chunked
- Chunking is transparent to the application layer
- The receiver automatically reassembles chunks before decrypting
This library uses the same binary protocol as:
- bitchat (iOS)
- bitchat-android
Messages can be exchanged between all three platforms.
MIT