No description
Find a file
2026-06-08 12:25:26 +03:00
.dart_tool init 2026-06-07 15:05:57 +03:00
lib nostr account actor 2026-06-08 12:25:26 +03:00
test nostr account actor 2026-06-08 12:25:26 +03:00
.gitignore file actor 2026-06-07 15:26:11 +03:00
analysis_options.yaml file actor 2026-06-07 15:26:11 +03:00
devbox.json init 2026-06-07 15:05:57 +03:00
devbox.lock init 2026-06-07 15:05:57 +03:00
GUIDELINES.md code guidelines 2026-06-07 15:22:41 +03:00
pubspec.lock basic nostr actors 2026-06-07 16:45:43 +03:00
pubspec.yaml file upload actor 2026-06-08 11:08:00 +03:00
README.md nostr account actor 2026-06-08 12:25:26 +03:00

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:

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.

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
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.

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).

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

NostrPublishActor

Publishes signed Nostr events to one or more relays and collects OK/rejection outcomes. Retries failed relay connections with exponential back-off.

final actor = NostrPublishActor(
  relayUrls: ['wss://relay.example.com'],
  retryConfig: const RetryConfig(),   // optional
  batchTimeout: const Duration(seconds: 10),  // optional
);
Parameter Type Default Description
relayUrls List<String> 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).

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.

final actor = NostrFetchActor(
  relayUrls: ['wss://relay.example.com'],
  eoseDeadline: const Duration(seconds: 5),  // optional
);
Parameter Type Default Description
relayUrls List<String> 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
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 library.

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<String, String> with keys NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, NORTHWEST, CENTRAL
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

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.

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.

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/<app>/<pubkey>/<uuid>.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.

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<String, dynamic> 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.

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'}