NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #3693 most downloaded on pub.dev
Pure-Dart Nostr protocol library. Events, signing, NIP-44 encryption, gift wrap, and 35+ NIPs. Transport-agnostic, Flutter Web compatible.
Last release 4 months ago
22 May 2026
Ships unpredictably
gaps range from 9 days to 2.4 years
Nearly every release is documented
notes for 15 of 15 stable releases
Nothing withdrawn
no release was ever pulled
4 years old
15 releases · first in 2022
One column per quarter.
Happy 🍕 day Full Changelog : v1.5.0...v2.0.0
Happy 🍕 day
Full Changelog: v1.5.0...v2.0.0
First major rewrite since v1.5.0. The library is now pure-protocol (no transport / WebSocket dependency), Flutter Web compatible, and spec-aligned against the upstream nostr-protocol/nips master. NIP-04 plaintext DMs are gone, every NIP has typed parse output, and all crypto runs through Schnorr / Encryption (no direct bip340).
NIP-04 removed — Use Nip17 / DirectMessage (NIP-17 over NIP-59 gift wrap) instead.
Parameter privkey renamed to secretKey across the entire API (Event.from, all NIP encode methods, Nip59.wrap/unwrap, etc.)
Core API:
| Before | After |
|---|---|
Keychain(privkey) |
Keys(privkey) |
keychain.private |
keys.secret |
Event.fromJson(Map) |
Event.fromMap(Map) |
Event.toJson() → Map |
Event.toMap() |
Event.deserialize(dynamic) |
Event.deserialize(String) |
Event mutable fields (late) |
Event immutable (final); use Event.copyWith(...) or Event.unsigned(...) |
Filter.fromJson(Map) |
Filter.fromMap(Map) |
Filter.toJson() → Map |
Filter.toMap() |
Request('id', [filter]) |
Request(subscriptionId: 'id', filters: [filter]) |
Filter(e: [...]) |
Filter(eTags: [...]) |
Filter(p: [...]) |
Filter(pTags: [...]) |
Filter(a: [...]) |
Filter(aTags: [...]) |
Zap.request(amount: int) |
Zap.request(amount: BigInt) |
ZapRequestData.amount: int? |
ZapRequestData.amount: BigInt? |
MessageType.name |
MessageType.label |
generate64RandomHexChars() |
generateRandomHex() |
Event.fromJson/toJsonandFilter.fromJson/toJsonnow match the rest of the library:fromJson(String)returns an Event/Filter,toJson()returns a JSON string. The Map variants live on the newfromMap/toMapnames. SeeMIGRATION.md§2 + §22 for details.
Eventis now fully immutable — every field isfinal. UseEvent.unsigned(...)to build an event with a precomputed id and empty sig (NIP-17 rumors, NIP-13 mining probes), andevent.copyWith(...)to derive a modified copy. The old "mutatepartialEvent.id = ..." idiom no longer compiles. SeeMIGRATION.md§23.
Zap.request/anonymousRequest/privateRequestnow takeamount: BigInt?(wasint?).ZapRequestData.amountis alsoBigInt?. This keeps precision for amounts > 2^53 millisats when the library is compiled to JavaScript (Flutter Web). SeeMIGRATION.md§24.
NIP classes renamed — domain name is now the primary class, Nip* is the alias:
| Before | After | Alias |
|---|---|---|
Nip1 |
Note |
typedef Nip1 = Note |
Nip2 |
FollowList |
typedef Nip2 = FollowList |
Nip5 |
DnsIdentifier |
typedef Nip5 = DnsIdentifier |
Nip9 |
Deletion |
typedef Nip9 = Deletion |
Nip10 |
Threading |
typedef Nip10 = Threading |
Nip13 |
ProofOfWork |
typedef Nip13 = ProofOfWork |
Nip17 |
DirectMessage |
typedef Nip17 = DirectMessage |
Nip18 |
Repost |
typedef Nip18 = Repost |
Nip19 |
Bech32Entity |
typedef Nip19 = Bech32Entity |
Nip20 |
CommandResult |
typedef Nip20 = CommandResult |
Nip21 |
NostrUri |
typedef Nip21 = NostrUri |
Nip22 |
Comment |
typedef Nip22 = Comment |
Nip23 |
Article |
typedef Nip23 = Article |
Nip25 |
Reaction |
typedef Nip25 = Reaction |
Nip28 |
PublicChat |
typedef Nip28 = PublicChat |
Nip29 |
Group |
typedef Nip29 = Group |
Nip32 |
Label |
typedef Nip32 = Label |
Nip38 |
UserStatus |
typedef Nip38 = UserStatus |
Nip42 |
RelayAuth |
typedef Nip42 = RelayAuth |
Nip44 |
Encryption |
typedef Nip44 = Encryption |
Nip46 |
NostrConnect |
typedef Nip46 = NostrConnect |
Nip47 |
WalletConnect |
typedef Nip47 = WalletConnect |
Nip51 |
UserList |
typedef Nip51 = UserList |
Nip53 |
LiveActivity |
typedef Nip53 = LiveActivity |
Nip57 |
Zap |
typedef Nip57 = Zap |
Nip59 |
GiftWrap |
typedef Nip59 = GiftWrap |
Nip65 |
RelayList |
typedef Nip65 = RelayList |
Nip72 |
ModeratedCommunity |
typedef Nip72 = ModeratedCommunity |
Nip89 |
AppHandler |
typedef Nip89 = AppHandler |
Methods renamed — encode()→create(), decode()→parse(), spec-aligned verbs:
| Before | After |
|---|---|
Nip1.encodeTextNote() |
Note.create() |
Nip1.encodeSetMetadata() |
Note.setMetadata() |
Nip1.decodeTextNote() |
Note.parse() |
Nip2.encode() |
FollowList.create() |
Nip2.decode() |
FollowList.parse() |
Nip5.encode() |
DnsIdentifier.create() |
Nip5.decode() |
DnsIdentifier.parse() |
Nip9.encode() |
Deletion.create() |
Nip9.decode() |
Deletion.parse() |
Nip10.fromTags(tags) |
Threading.parseTags(tags) |
Nip25.encode() |
Reaction.create() |
Nip25.decode() |
Reaction.parse() |
Nip28.createChannel() |
PublicChat.channel() |
Nip28.setChannelMetaData() |
PublicChat.channelMetadata() |
Nip28.sendChannelMessage() |
PublicChat.channelMessage() |
Nip28.hideChannelMessage() |
PublicChat.hideMessage() |
Nip28.muteUser() |
PublicChat.muteUser() |
Nip28.getChannelCreation() |
PublicChat.parseChannel() |
Nip28.getChannelMetadata() |
PublicChat.parseMetadata() |
Nip28.getChannelMessage() |
PublicChat.parseMessage() |
Nip28.getMessageHidden() |
PublicChat.parseHidden() |
Nip28.getUserMuted() |
PublicChat.parseMuted() |
Nip47.encodeRequest() |
WalletConnect.request() |
Nip47.decodeInfo() |
WalletConnect.parseInfo() |
Nip51.createMutePeople() |
UserList.mutePeople() |
Nip51.createPinEvent() |
UserList.pinEvent() |
Nip51.createCategorizedPeople() |
UserList.categorizedPeople() |
Nip51.createCategorizedBookmarks() |
UserList.categorizedBookmarks() |
Nip51.peoplesToTags() |
UserList.contactsToTags() |
Nip51.peoplesToContent() |
UserList.contactsToContent() |
Nip51.getLists(event, secretKey) |
UserList.parse(event, secretKey: ...) |
Nip57.encodeZapRequest() |
Zap.request() |
Nip57.decodeZapReceipt() |
Zap.parseReceipt() |
All model classes renamed with Data suffix:
| Before | After |
|---|---|
Note |
NoteData |
Profile |
ProfileData |
DNS |
DnsData |
DeletionRequest |
DeletionRequestData |
Reaction (model) |
ReactionData |
Repost (model) |
RepostData |
Comment (model) |
CommentData |
Nip23Article |
ArticleData |
Channel |
ChannelData |
ChannelMessage |
ChannelMessageData |
ChannelMessageHidden |
ChannelMessageHiddenData |
ChannelUserMuted |
ChannelUserMutedData |
UserStatus (model) |
UserStatusData |
LiveActivity (model) |
LiveActivityData |
ZapRequest / ZapReceipt |
ZapRequestData / ZapReceiptData |
ShareableIdentifiers |
ShareableIdentifierData |
UserList |
UserListData |
Event-kind constants standardised to kindXxx prefix on every NIP
class (e.g. Zap.kindZapRequest, WalletConnect.kindWalletInfo,
ModeratedCommunity.kindCommunity, AppHandler.kindHandlerInfo,
NostrConnect.kindNostrConnect, Deletion.kindDeletion).
Signature changes (same name, different shape):
| Before | After |
|---|---|
Close.deserialize(dynamic) |
Close.deserialize(String payload) |
Eose.deserialize(dynamic) |
Eose.deserialize(String payload) |
Request.deserialize(dynamic) |
Request.deserialize(String payload) |
Message.deserialize(dynamic) |
Message.deserialize(String payload) |
Nip20.deserialize(dynamic) |
CommandResult.deserialize(String payload) |
MessageType.fromName(String) |
MessageType.from(String) |
Keychain.sign(String message) |
Keys.sign({required String message}) |
UserList.parse(event, privkey) (sync) |
UserList.parse(event, {required secretKey}) (async, named arg) |
UserList.fromContent(...) (sync) |
UserList.fromContent(...) (async) |
Event.from(secretKey, kind, tags, content, createdAt) |
Event.from({required kind, required content, required secretKey, tags?, createdAt?, …}) |
Removed without direct replacement:
| Removed | Migration |
|---|---|
Keychain class |
Use Keys |
Keychain.verify(pubkey, message, sig) |
Use Schnorr.verify(...) |
Nip4 / EncryptedDirectMessage (NIP-04) |
Use DirectMessage (NIP-17 over NIP-59) |
Nip19.encodePubkey/encodePrivkey/encodeNote |
Use Bech32Entity.encode(prefix: ..., data: ...) |
Nip19.decodePubkey/decodePrivkey/decodeNote |
Use Bech32Entity.decode(payload: ...) or Bech32Entity.decodeAny(...) |
kepler.dart, crypto/operator.dart, crypto/nip_004.dart |
Internal NIP-04 helpers, gone with NIP-04 |
Contact.aliasPubKey field |
Field removed; Contact(pubkey, mainRelay, petName) is 3-arg |
Other breaking changes:
| Before | After |
|---|---|
Filter fields mutable |
Filter fields final, constructor const |
| All model positional ctors | All model named const constructors |
bip340 re-exported via package:nostr |
Internal; use Schnorr.sign / verify / derivePublicKey |
nip_044_utils.dart re-exported |
Internal; use Encryption.encrypt / decrypt |
Keys.nsec / Keys.npub gettersKeys() now validates exact 64-char hex lengthMessageType.closed (CLOSED relay message per NIP-01)FollowList.create() (kind-3 follow list events)Article.create() (kind-30023 / 30024 long-form events)DnsIdentifier.verify() DNS identity verification with no-redirect per specDnsIdentifier.verificationUrl() helperDeletion now supports a tags (addressable events) and k tags (kind indication)NostrUri.encode() rejects nsec identifiers per specUserList.parse() handles both plaintext JSON and NIP-44 encrypted contentTextNote, Profile, DirectMessage, etc.)Tag = List<String> and Tags = List<Tag> typedefsFilter.tagFilters: Map<String, List<String>>? — generic
single-letter tag filter map (#d / #t / #k / #r, etc.).
Filter.fromJson collects every #X key into this map;
eTags / aTags / pTags still take precedence when set.nonceTag(value, target), targetFromTag,
meetsTarget(event), and mine(difficulty, kind, content, secretKey, ...) for actually producing PoW events. Previously only
countLeadingZeroes was exposed.RelayInfo.fetch(relayUrl)
returns RelayInfoData with supportedNips, limitation,
software, version, and operator contact fields. URL scheme
rewritten from wss:// / ws:// to https:// / http://
automatically. Tolerant of wrong-typed fields commonly seen in
the wild.create, validate,
payloadHash, toAuthHeader / fromAuthHeader.parseProfileBadges accepts the legacy kind 30008 form too.message, threadRoot, threadReply,
joinRequest, leaveRequest) and parsers for parseAdmins
(kind 39001) / parseMembers (kind 39002).MissingTagException permissive mode — every parser whose NIP
defines spec-required tags (NIPs 22, 23, 29, 38, 53, 57, 58, 72, 89,
94, 98) now accepts {bool permissive = false}; in permissive mode
the missing-tag set is recorded on <Data>.missingTags and
<Data>.isComplete instead of throwing, so consumers can still
display whatever is salvageable on the ~31 % of real-world events
that violate spec requirements.HIGH (security / correctness)
unpad): enforce padded.length == 2 + calcPaddedLen(unpaddedLen)
per spec pseudocode — prevents accepting malleable / over-sized padded
buffers.encodeShareableIdentifiers / decodeShareableIdentifiers):
switch naddr identifier and relay byte encoding from String.codeUnits
(UTF-16) to utf8.encode / utf8.decode. Matches rust-nostr and
nostr-tools; unblocks non-ASCII d-tags (e.g. café, 日本, emoji).fromAuthHeader, validate): fromAuthHeader verifies
id + signature on the decoded event. validate calls
event.isValid() first for defense in depth.validate) and NIP-59 (unwrap) call
event.isValid() at the top so forged events are caught before any
request- or decrypt-specific check.Spec gaps closed
parse): throws MissingTagException when required K,
k, root-scope (E/A/I), or parent (e/a/i) tags are absent.parse): when multiple e/p tags exist, the target is
the LAST one per spec. Surfaces the e relay hint and optional a.parseMetadata): throws on missing d tag (group
identifier — required by NIP-01 for addressable events).parse): unknown markers fall back to read+write instead
of silently dropping the relay.approval): approvedEventJson required when referenced
via e (spec MUST). Added approvedEventCoord for addressable
posts; rejects when neither (or both) of e/a is provided.parseHandlerInfo): platform-handler detection uses a
positive allowlist (web, ios, android, iphone, ipad,
macos, linux, windows) instead of a brittle exclude-list.encodeShareableIdentifiers requires author and kind
for naddr. 5000-char cap enforced on encode and decode. Added
decodeAny() dispatcher.Exception contract — all errors are now NostrException
Every public deserialization / decode entrypoint that previously could
leak a raw FormatException, _TypeError, or package:bech32
exception now wraps it as a NostrException subclass, matching the
documented contract in error.dart. Callers only need on NostrException.
Bech32Entity.decode / decodeAny / encode — wrap
package:bech32 errors (InvalidChecksum, MixedCase,
TooShortChecksum, TooLong, InvalidSeparator) and non-hex input
as DeserializationException. The underlying message is suppressed
in the wrapped error to avoid echoing candidate secrets to logs.Schnorr.derivePublicKey / sign / verify — non-hex inputs
now throw InvalidKeyException instead of leaking FormatException
from hex.decode.Event.fromJson — bad JSON or non-object payloads throw
DeserializationException.Event.deserialize — validates the wire frame starts with
"EVENT" and has the right shape; previously accepted any tag
silently and threw _TypeError on shape mismatches.Close / Eose / Request / Message / CommandResult .deserialize
— wrap json.decode failures and validate the frame tag + shape
before any cast.Filter.fromJson — typed validation on every field; passing
e.g. kinds: "not a list" now throws DeserializationException
instead of _TypeError.PublicChat.parseChannel / parseMetadata — non-JSON or
non-object content throws DeserializationException.ProofOfWork.countLeadingZeroes — non-hex input throws
DeserializationException instead of FormatException from
int.parse.GiftWrap.unwrap — wraps json.decode failure on the inner
rumor payload as DeserializationException.ModeratedCommunity.parseApproval / AppHandler.parseHandlerInfo
/ Zap.parseReceipt — a JSON array (or any non-object) in a
content / description slot no longer raises _TypeError; the
optional embedded field is left null, matching the existing fail-soft
contract.Error messages no longer echo candidate secrets
Keys(...) rejection no longer includes the input string —
package:bech32's MixedCase error message would otherwise leak
the candidate (e.g. a confused caller passing their nsec) into logs.InvalidNostrUriException message no longer embeds the input
(the raw value remains on the typed .input field for consumers
that genuinely need it).Performance
mine no longer Schnorr-signs every nonce iteration.
Mining is hash-bound; computing the candidate event id from the
canonical serialization is ~1000× faster than signing. The winning
nonce is signed once at the end. Previously high-difficulty PoW was
effectively unreachable.encodeShareableIdentifiers no longer O(n²) in relay
count. Rebuilt the TLV with a StringBuffer instead of repeated
string concat — 100 000 relays went from ~80 s to ~300 ms (after
which the 5000-char length cap rejects).Real bugs
parseChannel / parseMetadata): channel content with
the spec-defined relays array no longer crashes
Map<String, String>.from. ChannelData exposes
relays: List<String> separately from string additional.decode): rejects nostr:nsec1… and any prefix outside
{npub, note, nprofile, nevent, naddr}.nip_002.dart,
nip_028.dart, nip_065.dart._randomPastTimestamp now covers the full 2-day window (was ~172 seconds)RangeError)Encryption.decrypt and validate canonical MAC + padding errorsisValidName now allows hyphens and dots per specparse wraps malformed-JSON content as
DeserializationException and no longer echoes the content in the
error messagekind parameter of
encodeShareableIdentifiers — kind = 2^32 would round-trip to
0. Now rejects out-of-range kinds with InvalidArgumentExceptionZap.request / anonymousRequest / privateRequest
reject negative amount values (spec is unsigned millisats)NoteData.thread typed as non-nullable Thread (was
Thread?) — Note.parse was always returning a non-null sentinel,
contradicting the declared type. See MIGRATION.md.parseMetadata now reports isPrivate, isClosed,
and isBroadcast flag presence in addition to isOpen / isPublic_getTagValue helpers with shared findTagValueEose and Nip20Constants
Repost.kindRepost (= 6) and Repost.kindGenericRepost (= 16)
added so callers can avoid magic numbers when parsing NIP-18 events.Dependencies
pointycastle constraint loosened to >=3.7.3 <5.0.0 — picks up
v4.0.0 (now resolved) which drops the discontinued js transitive
package.Spec tightening
UserList.parse) now rejects events whose kind is
outside the list ranges (10000-10999 or 30000-39999) with
InvalidKindException. Previously it accepted any kind silently
and would happily mangle a NIP-23 article into a list.bip340 direct imports removed from event.dart and keys.dart;
both route through Schnorr. Added Schnorr.derivePublicKey with
32-byte input validation. Event.isValid catches
InvalidKeyException and returns false instead of propagating.nip_044_utils.dart no longer re-exported from nostr.dart. The
raw crypto primitives (pad, unpad, chacha20, hkdf,
calculateMac, parsePayload, etc.) are easy to misuse and now stay
internal. Use Encryption.encrypt / Encryption.decrypt. Tests that
need the primitives import the file path directly.feat: add EOSE class to obtain subscriptionId by @uchijo in #41
Full Changelog: v1.4.3...v1.5.0
added MessageType enum and concreteType to Message by @uchijo in #39
Full Changelog: v1.4.2...v1.4.3
feat: NIP-50 search capability by @jsun1 in #36
Full Changelog : v1.4.0...v1.4.1
Full Changelog: v1.4.0...v1.4.1
NIP 04 Encrypted Direct Message
Full Changelog: v1.3.4...v1.4.0
Special thanks to @no-prob for his contribution to NIP 4
Special thanks to @water783 for his contributions to NIP 5, 10, 15, 19, 20, 28, 51
v1.3.3 to v1.3.4 by @ethicnology in #21
Full Changelog: v1.3.3...v1.3.4
thanks to @ryzizub 🥇 for:
Thanks @sebdeveloper6952 for the typo
Thanks @sebdeveloper6952 for the typo
Full Changelog: v1.3.2...v1.3.3
Develop by @ethicnology in #14 @ryzizub mentioned event checks/assertions can be computationally intensive with tons of events. This merge provides a
Full Changelog: v1.3.1...v1.3.2
fix: event tags parsing by @ryzizub in #9
Full Changelog: v1.3.0...v1.3.1
add Message wrapper deserializer (+ unit tests)
- add Filters (+ unit tests) - add Request (+ unit tests) - Documentation
deserialization of NOSTR formatted events with or without subscription_id
- Fix createdAt initialization - Add asserts - Code comments
- Initial version.
Your coding agent can read these notes before it upgrades. Set up the MCP server →