2026-06-07 16:45:43 +03:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
|
|
import 'package:bip340/bip340.dart' as bip340;
|
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
|
import 'package:swarm/swarm.dart';
|
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
|
|
|
|
|
|
const _privKey =
|
|
|
|
|
'0000000000000000000000000000000000000000000000000000000000000001';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late NostrSignerActor signer;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
signer = NostrSignerActor(_privKey);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('GetPublicKey returns correct pubkey', () async {
|
|
|
|
|
final result = await signer.handle(const GetPublicKey());
|
|
|
|
|
expect(result, isA<PublicKeyResult>());
|
|
|
|
|
expect(
|
|
|
|
|
(result as PublicKeyResult).publicKey,
|
|
|
|
|
equals(bip340.getPublicKey(_privKey)),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('SignEvent returns EventSigned', () async {
|
|
|
|
|
final result = await signer.handle(
|
|
|
|
|
const SignEvent(kind: 1, content: 'hello'),
|
|
|
|
|
);
|
|
|
|
|
expect(result, isA<EventSigned>());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('signed event has all required fields', () async {
|
|
|
|
|
final result = await signer.handle(
|
|
|
|
|
const SignEvent(kind: 1, tags: [], content: 'test content'),
|
|
|
|
|
);
|
|
|
|
|
final event =
|
|
|
|
|
jsonDecode((result as EventSigned).event) as Map<String, dynamic>;
|
|
|
|
|
expect(event.containsKey('id'), isTrue);
|
|
|
|
|
expect(event.containsKey('pubkey'), isTrue);
|
|
|
|
|
expect(event.containsKey('created_at'), isTrue);
|
|
|
|
|
expect(event.containsKey('kind'), isTrue);
|
|
|
|
|
expect(event.containsKey('tags'), isTrue);
|
|
|
|
|
expect(event.containsKey('content'), isTrue);
|
|
|
|
|
expect(event.containsKey('sig'), isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('event id is correct SHA-256', () async {
|
|
|
|
|
const kind = 1;
|
|
|
|
|
const content = 'sha256 test';
|
|
|
|
|
const tags = <List<String>>[];
|
|
|
|
|
const ts = 1700000000;
|
|
|
|
|
|
|
|
|
|
final result = await signer.handle(
|
|
|
|
|
const SignEvent(kind: kind, tags: tags, content: content, createdAt: ts),
|
|
|
|
|
);
|
|
|
|
|
final event =
|
|
|
|
|
jsonDecode((result as EventSigned).event) as Map<String, dynamic>;
|
|
|
|
|
|
|
|
|
|
final pubkey = bip340.getPublicKey(_privKey);
|
|
|
|
|
final ser = jsonEncode([0, pubkey, ts, kind, tags, content]);
|
|
|
|
|
final expectedId = sha256
|
|
|
|
|
.convert(utf8.encode(ser))
|
|
|
|
|
.bytes
|
|
|
|
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
|
|
|
|
.join();
|
|
|
|
|
|
|
|
|
|
expect(event['id'], equals(expectedId));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('signature verifies', () async {
|
|
|
|
|
final result = await signer.handle(
|
|
|
|
|
const SignEvent(kind: 1, content: 'verify me', createdAt: 1700000000),
|
|
|
|
|
);
|
|
|
|
|
final event =
|
|
|
|
|
jsonDecode((result as EventSigned).event) as Map<String, dynamic>;
|
|
|
|
|
final pubkey = event['pubkey'] as String;
|
|
|
|
|
final id = event['id'] as String;
|
|
|
|
|
final sig = event['sig'] as String;
|
|
|
|
|
|
|
|
|
|
expect(bip340.verify(pubkey, id, sig), isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('createdAt is used when provided', () async {
|
|
|
|
|
const ts = 1234567890;
|
|
|
|
|
final result = await signer.handle(
|
|
|
|
|
const SignEvent(kind: 1, content: 'timestamp test', createdAt: ts),
|
|
|
|
|
);
|
|
|
|
|
final event =
|
|
|
|
|
jsonDecode((result as EventSigned).event) as Map<String, dynamic>;
|
|
|
|
|
expect(event['created_at'], equals(ts));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('createdAt defaults to now', () async {
|
|
|
|
|
final before = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
|
|
|
final result = await signer.handle(
|
|
|
|
|
const SignEvent(kind: 1, content: 'now test'),
|
|
|
|
|
);
|
|
|
|
|
final after = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
|
|
|
final event =
|
|
|
|
|
jsonDecode((result as EventSigned).event) as Map<String, dynamic>;
|
|
|
|
|
final ts = event['created_at'] as int;
|
|
|
|
|
expect(ts, greaterThanOrEqualTo(before));
|
|
|
|
|
expect(ts, lessThanOrEqualTo(after + 2));
|
|
|
|
|
});
|
2026-07-11 17:37:24 +03:00
|
|
|
|
|
|
|
|
test('Nip44Encrypt round-trips to self', () async {
|
|
|
|
|
final plaintext = jsonEncode({'blur_content_warnings': 'false', 'x': '1'});
|
|
|
|
|
final encrypted = await signer.handle(Nip44Encrypt(plaintext)) as Nip44Text;
|
|
|
|
|
expect(encrypted.value, isNot(equals(plaintext)));
|
|
|
|
|
|
|
|
|
|
// Payload is base64 with the NIP-44 v2 version byte (0x02) first.
|
|
|
|
|
final bytes = base64.decode(encrypted.value);
|
|
|
|
|
expect(bytes.first, equals(0x02));
|
|
|
|
|
|
|
|
|
|
final decrypted =
|
|
|
|
|
await signer.handle(Nip44Decrypt(encrypted.value)) as Nip44Text;
|
|
|
|
|
expect(decrypted.value, equals(plaintext));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Nip44Decrypt with a foreign key fails', () async {
|
|
|
|
|
final encrypted =
|
|
|
|
|
await signer.handle(const Nip44Encrypt('secret')) as Nip44Text;
|
|
|
|
|
final other = NostrSignerActor(
|
|
|
|
|
'0000000000000000000000000000000000000000000000000000000000000002',
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
() => other.handle(Nip44Decrypt(encrypted.value)),
|
|
|
|
|
throwsA(anything),
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-06-07 16:45:43 +03:00
|
|
|
}
|