Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add json serializable #10

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 71 additions & 81 deletions lib/src/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import 'dart:convert';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:bip340/bip340.dart' as bip340;
import 'package:json_annotation/json_annotation.dart';
import 'package:nostr/src/utils.dart';

part 'event.g.dart';

/// The only object type that exists is the event, which has the following format on the wire:
///
/// - "id": "32-bytes hex-encoded sha256 of the the serialized event data"
Expand All @@ -16,6 +19,7 @@ import 'package:nostr/src/utils.dart';
/// ],
/// - "content": "arbitrary string",
/// - "sig": "64-bytes signature of the sha256 hash of the serialized event data, which is the same as the 'id' field"
@JsonSerializable()
class Event {
/// 32-bytes hex-encoded sha256 of the the serialized event data (hex)
late String id;
Expand All @@ -24,37 +28,42 @@ class Event {
late String pubkey;

/// unix timestamp in seconds
late int createdAt;
@JsonKey(
name: 'created_at',
)
int createdAt;

/// - 0: set_metadata: the content is set to a stringified JSON object {name: <username>, about: <string>, picture: <url, string>} describing the user who created the event. A relay may delete past set_metadata events once it gets a new one for the same pubkey.
/// - 1: text_note: the content is set to the text content of a note (anything the user wants to say). Non-plaintext notes should instead use kind 1000-10000 as described in NIP-16.
/// - 2: recommend_server: the content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to recommend to its followers.
late int kind;
final int kind;

/// The tags array can store a tag identifier as the first element of each subarray, plus arbitrary information afterward (always as strings).
///
/// This NIP defines "p" — meaning "pubkey", which points to a pubkey of someone that is referred to in the event —, and "e" — meaning "event", which points to the id of an event this event is quoting, replying to or referring to somehow.
late List<List<String>> tags;
final List<List<String>> tags;

/// arbitrary string
String content = "";
@JsonKey(defaultValue: '')
String content;

/// 64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field
late String sig;

/// subscription_id is a random string that should be used to represent a subscription.
@JsonKey(includeIfNull: false, toJson: null)
String? subscriptionId;

/// Default constructor
Event(
this.id,
this.pubkey,
this.createdAt,
this.kind,
this.tags,
this.content,
this.sig,
) {
this.sig, {
this.subscriptionId,
}) {
assert(createdAt.toString().length == 10);
assert(createdAt <= currentUnixTimestampSeconds());
pubkey = pubkey.toLowerCase();
Expand All @@ -63,47 +72,36 @@ class Event {
assert(bip340.verify(pubkey, id, sig));
}

/// Instanciate Event object from the minimum available data
Event.from(
{this.createdAt = 0,
required this.kind,
required this.tags,
required this.content,
required String privkey,
this.subscriptionId}) {
if (createdAt == 0) {
createdAt = currentUnixTimestampSeconds();
}
assert(createdAt.toString().length == 10);
assert(createdAt <= currentUnixTimestampSeconds());
pubkey = bip340.getPublicKey(privkey).toLowerCase();
id = getEventId();
sig = getSignature(privkey);
}
factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json);

/// Deserialize an event from a JSON
Event.fromJson(Map<String, dynamic> json, {this.subscriptionId}) {
id = json['id'];
pubkey = json['pubkey'];
createdAt = json['created_at'];
kind = json['kind'];
tags = (json['tags'] as List<dynamic>)
.map((e) => (e as List<dynamic>).map((e) => e as String).toList())
.toList();
content = json['content'];
sig = json['sig'];
Map<String, dynamic> toJson() => _$EventToJson(this);

/// To obtain the event.id, we sha256 the serialized event.
/// The serialization is done over the UTF-8 JSON-serialized string (with no white space or line breaks) of the following structure:
///
///[
/// 0,
/// <pubkey, as a (lowercase) hex string>,
/// <created_at, as a number>,
/// <kind, as a number>,
/// <tags, as an array of arrays of non-null strings>,
/// <content, as a string>
///]
String getEventId() {
List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content];
String serializedEvent = json.encode(data);
List<int> hash = sha256.convert(utf8.encode(serializedEvent)).bytes;
return hex.encode(hash);
}

/// Serialize an event in JSON
Map<String, dynamic> toJson() => {
'id': id,
'pubkey': pubkey,
'created_at': createdAt,
'kind': kind,
'tags': tags,
'content': content,
'sig': sig
};
/// Each user has a keypair. Signatures, public key, and encodings are done according to the Schnorr signatures standard for the curve secp256k1
/// 64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field
String getSignature(String privateKey) {
/// aux must be 32-bytes random bytes, generated at signature time.
/// https://github.com/nbd-wtf/dart-bip340/blob/master/lib/src/bip340.dart#L10
String aux = generate64RandomHexChars();
return bip340.sign(privateKey, id, aux);
}

/// Serialize to nostr event message
/// - ["EVENT", event JSON as defined above]
Expand All @@ -119,51 +117,43 @@ class Event {
/// Deserialize a nostr event message
/// - ["EVENT", event JSON as defined above]
/// - ["EVENT", subscription_id, event JSON as defined above]
Event.deserialize(input) {
factory Event.deserialize(input) {
Map<String, dynamic> json = {};
if (input.length == 2) {
json = input[1] as Map<String, dynamic>;

final event = _$EventFromJson(json);

return event;
} else if (input.length == 3) {
json = input[2] as Map<String, dynamic>;
subscriptionId = input[1];
json['subscriptionId'] = input[1];

final event = _$EventFromJson(json);

return event;
} else {
throw Exception('invalid input');
}
id = json['id'];
pubkey = json['pubkey'];
createdAt = json['created_at'];
kind = json['kind'];
tags = (json['tags'] as List<dynamic>)
.map((e) => (e as List<dynamic>).map((e) => e as String).toList())
.toList();
content = json['content'];
sig = json['sig'];
}

/// To obtain the event.id, we sha256 the serialized event.
/// The serialization is done over the UTF-8 JSON-serialized string (with no white space or line breaks) of the following structure:
///
///[
/// 0,
/// <pubkey, as a (lowercase) hex string>,
/// <created_at, as a number>,
/// <kind, as a number>,
/// <tags, as an array of arrays of non-null strings>,
/// <content, as a string>
///]
String getEventId() {
List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content];
String serializedEvent = json.encode(data);
List<int> hash = sha256.convert(utf8.encode(serializedEvent)).bytes;
return hex.encode(hash);
}

/// Each user has a keypair. Signatures, public key, and encodings are done according to the Schnorr signatures standard for the curve secp256k1
/// 64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field
String getSignature(String privateKey) {
/// aux must be 32-bytes random bytes, generated at signature time.
/// https://github.com/nbd-wtf/dart-bip340/blob/master/lib/src/bip340.dart#L10
String aux = generate64RandomHexChars();
return bip340.sign(privateKey, id, aux);
/// Instanciate Event object from the minimum available data
Event.from(
{this.createdAt = 0,
required this.kind,
required this.tags,
required this.content,
required String privkey,
this.subscriptionId}) {
if (createdAt == 0) {
createdAt = currentUnixTimestampSeconds();
} else {
createdAt = createdAt;
}
assert(createdAt.toString().length == 10);
assert(createdAt <= currentUnixTimestampSeconds());
pubkey = bip340.getPublicKey(privkey).toLowerCase();
id = getEventId();
sig = getSignature(privkey);
}
}
41 changes: 41 additions & 0 deletions lib/src/event.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 21 additions & 52 deletions lib/src/filter.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import 'package:json_annotation/json_annotation.dart';

part 'filter.g.dart';

/// filter is a JSON object that determines what events will be sent in that subscription
@JsonSerializable(includeIfNull: false)
class Filter {
/// a list of event ids or prefixes
List<String>? ids;
Expand All @@ -10,9 +15,11 @@ class Filter {
List<int>? kinds;

/// a list of event ids that are referenced in an "e" tag
@JsonKey(name: '#e')
List<String>? e;

/// a list of pubkeys that are referenced in a "p" tag
@JsonKey(name: '#p')
List<String>? p;

/// a timestamp, events must be newer than this to pass
Expand All @@ -25,56 +32,18 @@ class Filter {
int? limit;

/// Default constructor
Filter(
{this.ids,
this.authors,
this.kinds,
this.e,
this.p,
this.since,
this.until,
this.limit});

/// Deserialize a filter from a JSON
Filter.fromJson(Map<String, dynamic> json) {
ids = json['ids'] == null ? null : List<String>.from(json['ids']);
authors =
json['authors'] == null ? null : List<String>.from(json['authors']);
kinds = json['kinds'] == null ? null : List<int>.from(json['kinds']);
e = json['#e'] == null ? null : List<String>.from(json['#e']);
p = json['#p'] == null ? null : List<String>.from(json['#p']);
since = json['since'];
until = json['until'];
limit = json['limit'];
}

/// Serialize a filter in JSON
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (ids != null) {
data['ids'] = ids;
}
if (authors != null) {
data['authors'] = authors;
}
if (kinds != null) {
data['kinds'] = kinds;
}
if (e != null) {
data['#e'] = e;
}
if (p != null) {
data['#p'] = p;
}
if (since != null) {
data['since'] = since;
}
if (until != null) {
data['until'] = until;
}
if (limit != null) {
data['limit'] = limit;
}
return data;
}
Filter({
this.ids,
this.authors,
this.kinds,
this.e,
this.p,
this.since,
this.until,
this.limit,
});

factory Filter.fromJson(Map<String, dynamic> json) => _$FilterFromJson(json);

Map<String, dynamic> toJson() => _$FilterToJson(this);
}
39 changes: 39 additions & 0 deletions lib/src/filter.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading