nostr
Pure-Dart Nostr protocol library. Events, signing, NIP-44 encryption, gift wrap, and 35+ NIPs. Transport-agnostic, Flutter Web compatible.
2.0.0
4.8K downloads/mo
#3906 most downloaded on pub.dev
ethicnology/dart-nostr
What this package is like to depend on
Last release 3 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
1 release in the last 12 months
see the full history below
Release timeline
15 releases · Dec 2022 to May 2026Releases
latest 15-
2.0.022 May 2026Release notes
Open source →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/nipsmaster. NIP-04 plaintext DMs are gone, every NIP has typed parse output, and all crypto runs throughSchnorr/Encryption(no directbip340).Breaking Changes
NIP-04 removed — Use
Nip17/DirectMessage(NIP-17 over NIP-59 gift wrap) instead.Parameter
privkeyrenamed tosecretKeyacross the entire API (Event.from, all NIP encode methods,Nip59.wrap/unwrap, etc.)Core API:
Before After Keychain(privkey)Keys(privkey)keychain.privatekeys.secretEvent.fromJson(Map)Event.fromMap(Map)Event.toJson()→MapEvent.toMap()Event.deserialize(dynamic)Event.deserialize(String)Eventmutable fields (late)Eventimmutable (final); useEvent.copyWith(...)orEvent.unsigned(...)Filter.fromJson(Map)Filter.fromMap(Map)Filter.toJson()→MapFilter.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.nameMessageType.labelgenerate64RandomHexChars()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 Nip1Notetypedef Nip1 = NoteNip2FollowListtypedef Nip2 = FollowListNip5DnsIdentifiertypedef Nip5 = DnsIdentifierNip9Deletiontypedef Nip9 = DeletionNip10Threadingtypedef Nip10 = ThreadingNip13ProofOfWorktypedef Nip13 = ProofOfWorkNip17DirectMessagetypedef Nip17 = DirectMessageNip18Reposttypedef Nip18 = RepostNip19Bech32Entitytypedef Nip19 = Bech32EntityNip20CommandResulttypedef Nip20 = CommandResultNip21NostrUritypedef Nip21 = NostrUriNip22Commenttypedef Nip22 = CommentNip23Articletypedef Nip23 = ArticleNip25Reactiontypedef Nip25 = ReactionNip28PublicChattypedef Nip28 = PublicChatNip29Grouptypedef Nip29 = GroupNip32Labeltypedef Nip32 = LabelNip38UserStatustypedef Nip38 = UserStatusNip42RelayAuthtypedef Nip42 = RelayAuthNip44Encryptiontypedef Nip44 = EncryptionNip46NostrConnecttypedef Nip46 = NostrConnectNip47WalletConnecttypedef Nip47 = WalletConnectNip51UserListtypedef Nip51 = UserListNip53LiveActivitytypedef Nip53 = LiveActivityNip57Zaptypedef Nip57 = ZapNip59GiftWraptypedef Nip59 = GiftWrapNip65RelayListtypedef Nip65 = RelayListNip72ModeratedCommunitytypedef Nip72 = ModeratedCommunityNip89AppHandlertypedef Nip89 = AppHandlerMethods 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
Datasuffix:Before After NoteNoteDataProfileProfileDataDNSDnsDataDeletionRequestDeletionRequestDataReaction(model)ReactionDataRepost(model)RepostDataComment(model)CommentDataNip23ArticleArticleDataChannelChannelDataChannelMessageChannelMessageDataChannelMessageHiddenChannelMessageHiddenDataChannelUserMutedChannelUserMutedDataUserStatus(model)UserStatusDataLiveActivity(model)LiveActivityDataZapRequest/ZapReceiptZapRequestData/ZapReceiptDataShareableIdentifiersShareableIdentifierDataUserListUserListDataEvent-kind constants standardised to
kindXxxprefix 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 KeychainclassUse KeysKeychain.verify(pubkey, message, sig)Use Schnorr.verify(...)Nip4/EncryptedDirectMessage(NIP-04)Use DirectMessage(NIP-17 over NIP-59)Nip19.encodePubkey/encodePrivkey/encodeNoteUse Bech32Entity.encode(prefix: ..., data: ...)Nip19.decodePubkey/decodePrivkey/decodeNoteUse Bech32Entity.decode(payload: ...)orBech32Entity.decodeAny(...)kepler.dart,crypto/operator.dart,crypto/nip_004.dartInternal NIP-04 helpers, gone with NIP-04 Contact.aliasPubKeyfieldField removed; Contact(pubkey, mainRelay, petName)is 3-argOther breaking changes:
Before After Filterfields mutableFilterfieldsfinal, constructorconstAll model positional ctors All model named constconstructorsbip340re-exported viapackage:nostrInternal; use Schnorr.sign / verify / derivePublicKeynip_044_utils.dartre-exportedInternal; use Encryption.encrypt / decryptNew Features
Keys.nsec/Keys.npubgettersKeys()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()helperDeletionnow supportsatags (addressable events) andktags (kind indication)NostrUri.encode()rejectsnsecidentifiers per specUserList.parse()handles both plaintext JSON and NIP-44 encrypted content- Semantic typedef aliases for every NIP (
TextNote,Profile,DirectMessage, etc.) - New NIP implementations since v1.5.0: 11, 17, 18, 22, 23, 25, 27, 29, 32, 38, 40, 42, 44, 46, 47, 51 (expanded), 53, 57, 58, 59, 65, 72, 89, 94, 98
- Top-level
Tag = List<String>andTags = List<Tag>typedefs Filter.tagFilters: Map<String, List<String>>?— generic single-letter tag filter map (#d/#t/#k/#r, etc.).Filter.fromJsoncollects every#Xkey into this map;eTags/aTags/pTagsstill take precedence when set.- NIP-13 mining —
nonceTag(value, target),targetFromTag,meetsTarget(event), andmine(difficulty, kind, content, secretKey, ...)for actually producing PoW events. Previously onlycountLeadingZeroeswas exposed. - NIP-11 — relay information document:
RelayInfo.fetch(relayUrl)returnsRelayInfoDatawithsupportedNips,limitation,software,version, and operator contact fields. URL scheme rewritten fromwss:///ws://tohttps:///http://automatically. Tolerant of wrong-typed fields commonly seen in the wild. - NIP-94 — file metadata events (kind 1063).
- NIP-98 — HTTP auth (kind 27235):
create,validate,payloadHash,toAuthHeader/fromAuthHeader. - NIP-58 Profile Badges migrated to kind 10008 per spec;
parseProfileBadgesaccepts the legacy kind 30008 form too. - NIP-29 write helpers (
message,threadRoot,threadReply,joinRequest,leaveRequest) and parsers forparseAdmins(kind 39001) /parseMembers(kind 39002). MissingTagExceptionpermissive 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>.missingTagsand<Data>.isCompleteinstead of throwing, so consumers can still display whatever is salvageable on the ~31 % of real-world events that violate spec requirements.- rust-nostr cross-implementation test vectors (NIP-19, 13, 21, 44, 59, 05, 09)
- Real-world relay event fixture tests for 20+ event kinds
Bug Fixes & Spec Compliance
HIGH (security / correctness)
- NIP-44 (
unpad): enforcepadded.length == 2 + calcPaddedLen(unpaddedLen)per spec pseudocode — prevents accepting malleable / over-sized padded buffers. - NIP-19 (
encodeShareableIdentifiers/decodeShareableIdentifiers): switch naddr identifier and relay byte encoding fromString.codeUnits(UTF-16) toutf8.encode/utf8.decode. Matches rust-nostr and nostr-tools; unblocks non-ASCIId-tags (e.g.café,日本, emoji). - NIP-98 (
fromAuthHeader,validate):fromAuthHeaderverifies id + signature on the decoded event.validatecallsevent.isValid()first for defense in depth. - NIP-42 (
validate) and NIP-59 (unwrap) callevent.isValid()at the top so forged events are caught before any request- or decrypt-specific check.
Spec gaps closed
- NIP-22 (
parse): throwsMissingTagExceptionwhen requiredK,k, root-scope (E/A/I), or parent (e/a/i) tags are absent. - NIP-25 (
parse): when multiplee/ptags exist, the target is the LAST one per spec. Surfaces theerelay hint and optionala. - NIP-29 (
parseMetadata): throws on missingdtag (group identifier — required by NIP-01 for addressable events). - NIP-65 (
parse): unknown markers fall back to read+write instead of silently dropping the relay. - NIP-72 (
approval):approvedEventJsonrequired when referenced viae(spec MUST). AddedapprovedEventCoordfor addressable posts; rejects when neither (or both) ofe/ais provided. - NIP-89 (
parseHandlerInfo): platform-handler detection uses a positive allowlist (web,ios,android,iphone,ipad,macos,linux,windows) instead of a brittle exclude-list. - NIP-19:
encodeShareableIdentifiersrequiresauthorandkindfornaddr. 5000-char cap enforced on encode and decode. AddeddecodeAny()dispatcher.
Exception contract — all errors are now
NostrExceptionEvery public deserialization / decode entrypoint that previously could leak a raw
FormatException,_TypeError, orpackage:bech32exception now wraps it as aNostrExceptionsubclass, matching the documented contract inerror.dart. Callers only needon NostrException.Bech32Entity.decode/decodeAny/encode— wrappackage:bech32errors (InvalidChecksum,MixedCase,TooShortChecksum,TooLong,InvalidSeparator) and non-hex input asDeserializationException. The underlying message is suppressed in the wrapped error to avoid echoing candidate secrets to logs.Schnorr.derivePublicKey/sign/verify— non-hex inputs now throwInvalidKeyExceptioninstead of leakingFormatExceptionfromhex.decode.Event.fromJson— bad JSON or non-object payloads throwDeserializationException.Event.deserialize— validates the wire frame starts with"EVENT"and has the right shape; previously accepted any tag silently and threw_TypeErroron shape mismatches.Close/Eose/Request/Message/CommandResult.deserialize — wrapjson.decodefailures and validate the frame tag + shape before any cast.Filter.fromJson— typed validation on every field; passing e.g.kinds: "not a list"now throwsDeserializationExceptioninstead of_TypeError.PublicChat.parseChannel/parseMetadata— non-JSON or non-object content throwsDeserializationException.ProofOfWork.countLeadingZeroes— non-hex input throwsDeserializationExceptioninstead ofFormatExceptionfromint.parse.GiftWrap.unwrap— wrapsjson.decodefailure on the inner rumor payload asDeserializationException.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'sMixedCaseerror message would otherwise leak the candidate (e.g. a confused caller passing their nsec) into logs.InvalidNostrUriExceptionmessage no longer embeds the input (the raw value remains on the typed.inputfield for consumers that genuinely need it).
Performance
- NIP-13
mineno 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. - NIP-19
encodeShareableIdentifiersno longer O(n²) in relay count. Rebuilt the TLV with aStringBufferinstead of repeated string concat — 100 000 relays went from ~80 s to ~300 ms (after which the 5000-char length cap rejects).
Real bugs
- NIP-28 (
parseChannel/parseMetadata): channel content with the spec-definedrelaysarray no longer crashesMap<String, String>.from.ChannelDataexposesrelays: List<String>separately from stringadditional. - NIP-21 (
decode): rejectsnostr:nsec1…and any prefix outside{npub, note, nprofile, nevent, naddr}. - Tag-bounds bugs (RangeError on empty tags) fixed in
nip_002.dart,nip_028.dart,nip_065.dart. - fix(nip59):
_randomPastTimestampnow covers the full 2-day window (was ~172 seconds) - fix(nip10): bounds check on tags without markers (prevents
RangeError) - fix(nip44): stale error message expectations in test vectors — the
decrypt-fail vectors 004–008 were exercising a vestigial helper
that always tripped a fake version check; they now go through
Encryption.decryptand validate canonical MAC + padding errors - fix(nip28): safe null handling instead of force-unwraps on malformed events
- fix(nip05):
isValidNamenow allows hyphens and dots per spec - fix(nip05):
parsewraps malformed-JSON content asDeserializationExceptionand no longer echoes the content in the error message - fix(nip19): silent uint32 truncation on
kindparameter ofencodeShareableIdentifiers—kind = 2^32would round-trip to0. Now rejects out-of-range kinds withInvalidArgumentException - fix(nip57):
Zap.request/anonymousRequest/privateRequestreject negativeamountvalues (spec is unsigned millisats) - fix(nip01):
NoteData.threadtyped as non-nullableThread(wasThread?) —Note.parsewas always returning a non-null sentinel, contradicting the declared type. See MIGRATION.md. - fix(nip29):
parseMetadatanow reportsisPrivate,isClosed, andisBroadcastflag presence in addition toisOpen/isPublic - fix(nip23): replaced private
_getTagValuehelpers with sharedfindTagValue - fix: copy-paste doc errors in
EoseandNip20
Constants
Repost.kindRepost(= 6) andRepost.kindGenericRepost(= 16) added so callers can avoid magic numbers when parsing NIP-18 events.
Dependencies
pointycastleconstraint loosened to>=3.7.3 <5.0.0— picks up v4.0.0 (now resolved) which drops the discontinuedjstransitive package.
Spec tightening
- NIP-51 (
UserList.parse) now rejects events whosekindis outside the list ranges (10000-10999or30000-39999) withInvalidKindException. Previously it accepted any kind silently and would happily mangle a NIP-23 article into a list.
Architecture
bip340direct imports removed fromevent.dartandkeys.dart; both route throughSchnorr. AddedSchnorr.derivePublicKeywith 32-byte input validation.Event.isValidcatchesInvalidKeyExceptionand returns false instead of propagating.nip_044_utils.dartno longer re-exported fromnostr.dart. The raw crypto primitives (pad,unpad,chacha20,hkdf,calculateMac,parsePayload, etc.) are easy to misuse and now stay internal. UseEncryption.encrypt/Encryption.decrypt. Tests that need the primitives import the file path directly.
-
1.5.011 Dec 2023 -
1.4.321 Nov 2023Release notes
Open source →What's Changed
- added MessageType enum and concreteType to Message by @uchijo in #39
- v1.4.3 by @ethicnology in #40
New Contributors
Full Changelog: v1.4.2...v1.4.3
Release notes
Open source →- refactor: Message.type is made of an MessageType enum instead of a String
-
1.4.216 Oct 2023 -
1.4.101 Sep 2023 -
1.4.025 May 2023Release notes
Open source →What's Changed
- 1.4.0 by @ethicnology in #30
- NIP 04 Encrypted Direct Message
- NIP 05 Mapping Nostr keys to DNS-based internet identifiers
- NIP 10 Conventions for clients' use of e and p tags in text events
- NIP 15 End of Stored Events Notice
- NIP 19 bech32-encoded entities
- NIP 20 Command Results
- NIP 28 Public Chat
- NIP 51 Lists
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, 51Release notes
Open source →- NIP 04 Encrypted Direct Message
- NIP 05 Mapping Nostr keys to DNS-based internet identifiers
- NIP 10 Conventions for clients' use of e and p tags in text events
- NIP 15 End of Stored Events Notice
- NIP 19 bech32-encoded entities
- NIP 20 Command Results
- NIP 28 Public Chat
- NIP 51 Lists
-
1.3.414 Feb 2023Release notes
Open source →What's Changed
- v1.3.3 to v1.3.4 by @ethicnology in #21
Full Changelog: v1.3.3...v1.3.4
thanks to @ryzizub 🥇 for:
- fix: pending bip340 issue
- test: Update test to check public key
- refactor: Event partial and from to factories
Release notes
Open source →- fix: pending bip340 issue
- test: Update test to check public key
- refactor: Event partial and from to factories
-
1.3.310 Feb 2023Release notes
Open source →What's Changed
- v1.3.3 by @ethicnology in #18
Thanks @sebdeveloper6952 for the typo
Full Changelog: v1.3.2...v1.3.3
Release notes
Open source →- add comments about verify and fix typo
- nip 002 implementation, unit tests, examples and documentation
- Event.partial to init an empty event that you validate later, documentation
-
1.3.229 Jan 2023Release notes
Open source →What's Changed
- Develop by @ethicnology in #14
@ryzizub mentioned event checks/assertions can be computationally intensive with tons of events.
This merge provides a boolean parameter, verify, which allow the lib user to bypass these checks
this merge also replaces some event in unit tests inputs since the one used contained emoji encoding problems (problem found by @ryzizub)
Full Changelog: v1.3.1...v1.3.2
Release notes
Open source →- refactor: Event with optional verification
- remove tests with encoding problem
- improve coverage
- Develop by @ethicnology in #14
-
1.3.127 Jan 2023Release notes
Open source →What's Changed
- fix: event tags parsing by @ryzizub in #9
- Develop by @ethicnology in #12
New Contributors
Full Changelog: v1.3.0...v1.3.1
-
1.3.023 Jan 2023Release notes
Open source →- add Close (+ unit tests)
- add Message wrapper deserializer (+ unit tests)
- Documentation
-
1.2.018 Jan 2023 -
1.1.031 Dec 2022Release notes
Open source →- fix Event.fromJson
- add subscriptionId
- deserialization of NOSTR formatted events with or without subscription_id
- add unit tests for Event to improve coverage
- Create Keychain container for private/public keys to encapsulate bip340 and add handy methods.
- Documentation
-
1.0.128 Dec 2022 -
1.0.027 Dec 2022