nip46 remote signer actor

Drop-in replacement for NostrSignerActor that delegates signing to an
external signer over NIP-46 (bunker:// URI, NIP-44 encrypted kind-24133).
Includes 8 tests with a fully self-contained fake relay helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-06-08 15:27:31 +03:00
parent 4b48c27e31
commit 128190975e
4 changed files with 540 additions and 0 deletions

View file

@ -81,6 +81,55 @@ 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://<signerPubkey>?relay=wss://relay.nsec.app&secret=<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