npm package: @soapbox.pub/nostr-lora
Description: Reference implementation of [NIP-LR](./NIP.md): Nostr over LoRa.
Latest version: 0.1.1

# @soapbox.pub/nostr-lora

Reference implementation of [NIP-LR](./NIP.md): Nostr over LoRa.

Handles packet encoding/decoding, chunked transmission, reassembly,
retransmission requests, and GM (announce) messages. Designed for use with
MeshCore LoRa devices over Web Serial.

## Structure

```
nostr-lora          # core — LoRaTransport, SerialConnectionManager, etc.
nostr-lora/react    # React hook — useNostrLora
```

React is an optional peer dependency. The core entry point has no React
dependency.

## Installation

```sh
npm install @soapbox.pub/nostr-lora
```

## Usage

### React

```tsx
import { useNostrLora } from "nostr-lora/react";

function App() {
    const { connect, disconnect, isConnected, sendEvent, error } = useNostrLora(
        {
            onEvent(event) {
                console.log("received event", event);
            },
        },
    );

    return (
        <div>
            {isConnected
                ? <button onClick={disconnect}>Disconnect</button>
                : <button onClick={connect}>Connect</button>}
            {error && <p>{error.message}</p>}
        </div>
    );
}
```

`connect()` triggers the browser's serial port picker. Once connected, incoming
Nostr events are delivered via `onEvent`. Call `sendEvent(event)` to broadcast a
signed Nostr event over LoRa.

Full return type:

| Field              | Type                                                   | Description                               |
| ------------------ | ------------------------------------------------------ | ----------------------------------------- |
| `isConnected`      | `boolean`                                              | Whether the serial port is open           |
| `connecting`       | `boolean`                                              | `true` while the port is being opened     |
| `error`            | `Error \| null`                                        | Last error, if any                        |
| `portLabel`        | `string \| null`                                       | USB VID:PID of the connected device       |
| `deviceName`       | `string \| null`                                       | Device name from firmware                 |
| `sendQueue`        | `QueueSnapshot[]`                                      | Current outbound packet queue             |
| `lastPacket`       | `PacketReceiveInfo \| null`                            | SNR/RSSI/size of last received packet     |
| `connect`          | `() => Promise<void>`                                  | Open port and begin transport             |
| `disconnect`       | `() => Promise<void>`                                  | Close port                                |
| `sendEvent`        | `(event: NostrEvent) => Promise<void>`                 | Send a signed event                       |
| `sendGm`           | `(nodeId, eventCount?, recentSince?) => Promise<void>` | Broadcast a GM announcement               |
| `setTimingOptions` | `({ delay?, jitter? }) => void`                        | Adjust inter-packet timing live           |
| `transport`        | `LoRaTransport \| null`                                | Direct access to the underlying transport |

### Vanilla (no framework)

```ts
import { LoRaTransport, SerialConnectionManager } from "nostr-lora";

const connection = new SerialConnectionManager();
const transport = new LoRaTransport(connection);

transport.on("event:receive", (event) => {
    console.log("received event", event);
});

transport.on("connect", (portLabel) => {
    console.log("connected to", portLabel);
});

transport.on("error", (err) => {
    console.error(err);
});

// Triggers the browser port picker
await transport.begin();

// Send a signed Nostr event
await transport.sendEvent(signedEvent);

// Later
await transport.end();
```

### Custom connection

Implement `ConnectionManager` to use a different transport (e.g. WebSocket,
BLE):

```ts
import type { ConnectionManager, ConnectionManagerEvents } from "nostr-lora";
import { LoRaTransport } from "nostr-lora";

class MyConnection implements ConnectionManager {
    on<E extends keyof ConnectionManagerEvents>(
        event: E,
        handler: (...args: ConnectionManagerEvents[E]) => void,
    ) {/* ... */}

    async open() {/* ... */}
    async close() {/* ... */}
    async sendRawData(data: Uint8Array) {/* ... */}
}

const transport = new LoRaTransport(new MyConnection());
```

## Options

All options are optional. Defaults are exported as `Defaults`:

```ts
import { Defaults } from "nostr-lora";
```

| Option              | Default     | Description                                                 |
| ------------------- | ----------- | ----------------------------------------------------------- |
| `nodeId`            | `undefined` | Your node's identity bytes (used in GM packets)             |
| `logger`            | `null`      | Object with `log/warn/error/debug` methods (e.g. `console`) |
| `interPacketDelay`  | `2000` ms   | Base delay between packets in the send queue                |
| `interPacketJitter` | `500` ms    | Max random jitter added on top of the delay                 |
| `requestInactivity` | `30000` ms  | Idle time before sending a retransmission request           |
| `requestMaxRetries` | `3`         | Max retransmission attempts before abandoning               |
| `eventTimeout`      | `30000` ms  | Max wait for all chunks of a partial event                  |
| `dedupTimeout`      | `900000` ms | How long to remember received event IDs                     |
| `sentChunksTtl`     | `300000` ms | How long to keep sent chunks for potential retransmission   |
| `initialTtl`        | `6`         | Hop TTL assigned to locally-created events                  |
| `gmBackoffBase`     | `300000` ms | Minimum interval between GM responses to the same peer      |
| `gmMaxShareEvents`  | `3`         | Max events to re-share in response to a GM                  |
| `gmJitterMin`       | `500` ms    | Min random delay before responding to a GM                  |
| `gmJitterMax`       | `1500` ms   | Max random delay before responding to a GM                  |

## Events

`LoRaTransport` extends `EventEmitter` and emits:

| Event             | Args                                 | Description                                      |
| ----------------- | ------------------------------------ | ------------------------------------------------ |
| `connect`         | `portLabel: string`                  | Connection opened                                |
| `disconnect`      | —                                    | Connection closed                                |
| `error`           | `err: Error`                         | Error from connection or packet handling         |
| `event:receive`   | `event: NostrEvent`                  | A complete Nostr event was received and verified |
| `event:send`      | `event: NostrEvent`                  | A Nostr event finished sending                   |
| `packet:receive`  | `info: PacketReceiveInfo`            | Raw packet received (SNR, RSSI, size)            |
| `chunk:receive`   | `decoded, byteLength`                | Individual DATA chunk received                   |
| `gm:receive`      | `decoded, byteLength`                | GM packet received                               |
| `request:receive` | `prefixHex, missingChunks`           | Retransmission request received from peer        |
| `request:send`    | `eventIdHex, missingChunks, attempt` | Retransmission request sent                      |
| `queue:update`    | `snapshot: QueueSnapshot[]`          | Send queue changed                               |

## License

The source code of this library is provided to you under the terms of the
[MIT License](./LICENSE).