# swarm Actor-based business logic library. Each actor runs in an isolated Dart isolate; you send it a message and await a result. Actors are created with a factory tear-off so the handler is constructed inside the isolate: ```dart final actor = Actor.create(MyActor.new); final result = await actor.send(MyMessage()); await actor.stop(); ``` All examples below call `handle()` directly (no isolate), which is how the unit tests work. In production code use `Actor.create` + `send`. --- ## FileActor Local file I/O — text and binary. ```dart final actor = FileActor(); ``` No constructor parameters. | Message | Result | Description | |---|---|---| | `LoadFile(path)` | `FileLoaded(content)` | Read file as UTF-8 string | | `SaveFile(path, content)` | `FileSaved()` | Write string to file | | `LoadBinaryFile(path)` | `BinaryFileLoaded(bytes)` | Read file as `Uint8List` | | `SaveBinaryFile(path, bytes)` | `FileSaved()` (same type) | Write `Uint8List` to file | ```dart final actor = FileActor(); // Text await actor.handle(SaveFile('/tmp/note.txt', 'hello')); final r = await actor.handle(LoadFile('/tmp/note.txt')); print((r as FileLoaded).content); // hello // Binary await actor.handle(SaveBinaryFile('/tmp/data.bin', bytes)); final rb = await actor.handle(LoadBinaryFile('/tmp/data.bin')); final loaded = (rb as BinaryFileLoaded).bytes; ``` --- ## NostrSignerActor Signs Nostr events (NIP-01) using a secp256k1 private key. ```dart final actor = NostrSignerActor(privateKeyHex); ``` | Parameter | Type | Description | |---|---|---| | `privateKey` | `String` | 64-char hex private key | | Message | Result | Description | |---|---|---| | `SignEvent(kind, content, tags?, createdAt?)` | `EventSigned(event)` | Returns a signed event as a JSON string with all seven NIP-01 fields | | `GetPublicKey()` | `PublicKeyResult(publicKey)` | Returns the hex public key for this signer | `tags` defaults to `[]`. `createdAt` defaults to `DateTime.now()` (Unix seconds). ```dart final signer = NostrSignerActor(privkeyHex); final pk = await signer.handle(const GetPublicKey()); print((pk as PublicKeyResult).publicKey); final signed = await signer.handle( const SignEvent(kind: 1, content: 'hello world'), ); final eventJson = (signed as EventSigned).event; // ready to publish ``` --- ## Nip46SignerActor NIP-46 remote signer — drop-in replacement for `NostrSignerActor`. Delegates signing to an external signer (e.g. [Nsec.app](https://nsec.app)) over a Nostr relay using NIP-44 encrypted kind-24133 events. Accepts a `bunker://` URI. ```dart final actor = Nip46SignerActor( bunkerUri: 'bunker://?relay=wss://relay.nsec.app&secret=', ); ``` | Parameter | Type | Default | Description | |---|---|---|---| | `bunkerUri` | `String` | required | Bunker URI from the remote signer app | | `appPrivkey` | `String?` | random | App's ephemeral private key (injectable for tests) | | `connectionFactory` | `RelayConnection Function(String)?` | `WebSocketRelayConnection.new` | Override relay factory for tests | | `timeout` | `Duration` | 30 s | Per-request timeout | Implements the same `SignerMessage`/`SignerResult` protocol as `NostrSignerActor`: | Message | Result | Description | |---|---|---| | `GetPublicKey()` | `PublicKeyResult(publicKey)` | Returns the signer's public key (cached after first call) | | `SignEvent(kind, content, tags?, createdAt?)` | `EventSigned(event)` | Sends a `sign_event` request to the remote signer; returns the fully signed event JSON | Call `actor.close()` when done to tear down the relay connection. ```dart final signer = Nip46SignerActor( bunkerUri: bunkerUri, // scanned from QR or pasted by user ); final pk = await signer.handle(const GetPublicKey()); print((pk as PublicKeyResult).publicKey); final signed = await signer.handle( const SignEvent(kind: 1, content: 'hello from mobile'), ); final eventJson = (signed as EventSigned).event; await signer.close(); ``` The actor automatically sends a `connect` handshake on first use and reuses the relay connection for all subsequent requests. --- ## NostrPublishActor Publishes signed Nostr events to one or more relays and collects OK/rejection outcomes. Retries failed relay connections with exponential back-off. ```dart final actor = NostrPublishActor( relayUrls: ['wss://relay.example.com'], retryConfig: const RetryConfig(), // optional batchTimeout: const Duration(seconds: 10), // optional ); ``` | Parameter | Type | Default | Description | |---|---|---|---| | `relayUrls` | `List` | required | Relay WebSocket URLs | | `retryConfig` | `RetryConfig` | see below | Retry behaviour | | `batchTimeout` | `Duration` | 10 s | How long to wait for OK per relay | **RetryConfig** | Field | Default | Description | |---|---|---| | `maxAttempts` | 3 | Max connection attempts per relay | | `baseDelay` | 1 s | Initial retry delay | | `maxDelay` | 32 s | Cap on retry delay | | `jitter` | `true` | Add ±25 % randomisation to delay | | Message | Result | Description | |---|---|---| | `PublishBatch(events)` | `BatchPublished(outcomes)` | Publishes a list of JSON event strings; `outcomes` maps relay URL → `RelayConfirmed`, `RelayRejected(reason)`, or `RelayFailed` | Events must be fully signed JSON strings (e.g. from `NostrSignerActor`). ```dart final signer = NostrSignerActor(privkeyHex); final publisher = NostrPublishActor( relayUrls: ['wss://relay.otherwhere.app/'], ); final signed = await signer.handle( const SignEvent(kind: 1, content: 'hello'), ); final result = await publisher.handle( PublishBatch([(signed as EventSigned).event]), ); final outcomes = (result as BatchPublished).outcomes; for (final entry in outcomes.entries) { print('${entry.key}: ${entry.value}'); // wss://relay.otherwhere.app/: RelayConfirmed } ``` --- ## NostrFetchActor Fetches Nostr events from one or more relays using a NIP-01 filter. Collects events until EOSE or a deadline, deduplicates by event ID. ```dart final actor = NostrFetchActor( relayUrls: ['wss://relay.example.com'], eoseDeadline: const Duration(seconds: 5), // optional ); ``` | Parameter | Type | Default | Description | |---|---|---|---| | `relayUrls` | `List` | required | Relay WebSocket URLs | | `eoseDeadline` | `Duration` | 5 s | Max wait per relay for EOSE | | Message | Result | Description | |---|---|---| | `FetchEvents(filter)` | `EventsFetched(events)` | `filter` is a JSON string (NIP-01 filter object); `events` is a deduplicated list of JSON event strings | ```dart final fetcher = NostrFetchActor( relayUrls: ['wss://relay.otherwhere.app/'], ); final result = await fetcher.handle( FetchEvents('{"kinds":[1],"authors":["$pubkeyHex"],"limit":10}'), ); final events = (result as EventsFetched).events; for (final e in events) { print(jsonDecode(e)['content']); } ``` --- ## GeoActor Geohash encoding, decoding, and neighbour lookup via the [dart_geohash](https://pub.dev/packages/dart_geohash) library. ```dart final actor = GeoActor(); ``` No constructor parameters. | Message | Result | Description | |---|---|---| | `EncodeLocation(latitude, longitude, precision?)` | `GeohashEncoded(geohash)` | Encode lat/lon to geohash string; `precision` defaults to 9 | | `DecodeGeohash(geohash)` | `LocationDecoded(latitude, longitude)` | Decode geohash to coordinates | | `NeighborsOf(geohash)` | `NeighborsResult(neighbors)` | Returns a `Map` with keys `NORTH`, `NORTHEAST`, `EAST`, `SOUTHEAST`, `SOUTH`, `SOUTHWEST`, `WEST`, `NORTHWEST`, `CENTRAL` | ```dart final geo = GeoActor(); final enc = await geo.handle( const EncodeLocation(latitude: 48.8566, longitude: 2.3522, precision: 7), ); print((enc as GeohashEncoded).geohash); // u09tunq final dec = await geo.handle(DecodeGeohash('u09tunq')); print((dec as LocationDecoded).latitude); // ~48.856 print((dec as LocationDecoded).longitude); // ~2.352 final nb = await geo.handle(NeighborsOf('u09tunq')); print((nb as NeighborsResult).neighbors['NORTH']); // u09tunu ``` --- ## FileDownloadActor Downloads a file from a URL and saves it to disk via an injected `FileActor`. ```dart final actor = FileDownloadActor( fileActor: FileActor(), ); ``` | Parameter | Type | Default | Description | |---|---|---|---| | `fileActor` | `FileActor` | required | Actor used to write the downloaded bytes to disk | | `httpClient` | `http.Client?` | new client | Injectable HTTP client; provide a `MockClient` in tests | Call `actor.close()` when done to release the HTTP client. | Message | Result | Description | |---|---|---| | `DownloadFile(url, savePath)` | `FileDownloaded(savePath)` | GETs the URL and writes the response bytes to `savePath`. Throws if the response status is not 2xx. | ```dart final actor = FileDownloadActor(fileActor: FileActor()); final result = await actor.handle( const DownloadFile( url: 'https://example.com/image.jpg', savePath: '/tmp/image.jpg', ), ); print((result as FileDownloaded).savePath); // /tmp/image.jpg await actor.close(); ``` --- ## FileUploadActor Uploads files to a Nostr-authenticated remote storage service. Handles NIP-44 encrypted registration (kind 5392) and NIP-98 signed upload requests (kind 27235). Returns a permanent public CDN URL. ```dart final actor = FileUploadActor( privateKey: userPrivkeyHex, appName: kAppName, appSecret: kAppSecret, ); ``` | Parameter | Type | Default | Description | |---|---|---|---| | `privateKey` | `String` | required | User's 64-char hex Nostr private key | | `appName` | `String` | required | App identifier registered with the service | | `appSecret` | `String` | required | Per-app shared secret (keep out of public source) | | `apiBaseUrl` | `String` | `https://upload.otherwhere.app` | API base URL | | `relayUrl` | `String` | `wss://relay.otherwhere.app/` | Nostr relay for registration events | Call `actor.close()` when done to release HTTP connections. | Message | Result | Description | |---|---|---| | `RegisterForUpload()` | `UploaderRegistered()` | Publishes a kind 5392 event to the relay with the NIP-44 encrypted app secret. Call once per user; re-registering is safe (idempotent server-side). | | `UploadFile(bytes, contentType)` | `FileUploaded(cdnUrl)` | Requests a presigned PUT URL, uploads the file directly to storage, and returns the permanent CDN URL. Throws if `bytes.length > 10 MB`. Accepted content types: `image/jpeg`, `image/png`, `image/webp`, `image/gif`. | **Registration** should be called once and the result persisted (e.g. in `flutter_secure_storage`). If the flag is lost, calling it again is harmless. Do not block the upload flow on registration — unregistered users get a small daily upload allowance while the registration event propagates. ```dart const storage = FlutterSecureStorage(); const _kRegisteredKey = 'upload_registered_$kAppName'; final actor = FileUploadActor( privateKey: await storage.read(key: 'nostr_privkey'), appName: kAppName, appSecret: kAppSecret, ); // Register once if (await storage.read(key: _kRegisteredKey) != 'true') { await actor.handle(const RegisterForUpload()); await storage.write(key: _kRegisteredKey, value: 'true'); } // Upload final result = await actor.handle( UploadFile(bytes: imageBytes, contentType: 'image/jpeg'), ); final url = (result as FileUploaded).cdnUrl; // https://cdn.otherwhere.app///.jpg ``` --- ## NostrAccountActor Manages a Nostr user identity: keypair lifecycle, BIP-39 mnemonic support (NIP-06 derivation), and kind-0 profile fetch/publish via injected helper actors. ```dart final actor = NostrAccountActor( fetcher: NostrFetchActor(relayUrls: ['wss://relay.example.com']), publisher: NostrPublishActor(relayUrls: ['wss://relay.example.com']), ); ``` | Parameter | Type | Default | Description | |---|---|---|---| | `fetcher` | `NostrFetchActor?` | null | Required for `FetchProfile` | | `publisher` | `NostrPublishActor?` | null | Required for `UpdateProfile` | Both helpers are optional at construction; passing neither is valid when only using key management messages. | Message | Result | Description | |---|---|---| | `CreateAccount()` | `AccountCreated(publicKeyHex, mnemonic)` | Generates a new keypair from 128-bit entropy via BIP-39 + NIP-06 derivation | | `ImportAccount(privateKeyHex)` | `AccountImported(publicKeyHex)` | Loads an existing raw hex private key. `ExportMnemonic` returns null afterwards. | | `ImportMnemonic(mnemonic)` | `AccountImported(publicKeyHex)` | Derives keypair from a 12-word BIP-39 mnemonic via NIP-06 path `m/44'/1237'/0'/0/0` | | `GetAccountPublicKey()` | `AccountPublicKeyResult(publicKeyHex)` | Returns the loaded account's public key | | `ExportPrivateKey()` | `PrivateKeyExported(privateKeyHex)` | Returns the raw private key | | `ExportMnemonic()` | `MnemonicExported(mnemonic?)` | Returns the BIP-39 mnemonic, or null if the account was imported via raw private key | | `FetchProfile()` | `ProfileFetched(profile?)` | Fetches kind-0 from relay; caches result for `GetPublicData` | | `UpdateProfile(profile)` | `ProfileUpdated()` | Publishes a kind-0 event; caches profile for `GetPublicData` | | `GetPublicData()` | `PublicData(data)` | Returns a `Map` with `pubkey` plus any cached profile fields | **NostrProfile** fields (all optional): `name`, `displayName`, `about`, `picture`, `banner`, `website`, `nip05`, `lud16`. All messages except Create/Import throw `StateError` if called before the account is loaded. `FetchProfile` throws if no `fetcher` was provided; `UpdateProfile` throws if no `publisher` was provided. ```dart final fetcher = NostrFetchActor(relayUrls: ['wss://relay.otherwhere.app/']); final publisher = NostrPublishActor(relayUrls: ['wss://relay.otherwhere.app/']); final actor = NostrAccountActor(fetcher: fetcher, publisher: publisher); // New user final created = await actor.handle(const CreateAccount()) as AccountCreated; print(created.mnemonic); // 12-word backup phrase print(created.publicKeyHex); // hex pubkey // Returning user — restore from mnemonic await actor.handle(ImportMnemonic(savedMnemonic)); // Or restore from raw key await actor.handle(ImportAccount(savedPrivkeyHex)); // Update profile await actor.handle( UpdateProfile(const NostrProfile(name: 'alice', nip05: 'alice@example.com')), ); // Fetch latest profile from relay final fetched = await actor.handle(const FetchProfile()) as ProfileFetched; print(fetched.profile?.name); // Public-facing data (pubkey + cached profile) final pub = await actor.handle(const GetPublicData()) as PublicData; print(pub.data); // {'pubkey': '...', 'name': 'alice', 'nip05': 'alice@example.com'} ```